package freenet.node; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import freenet.client.DefaultMIMETypes; import freenet.io.comm.DMT; import freenet.io.comm.DisconnectedException; import freenet.io.comm.FreenetInetAddress; import freenet.io.comm.Message; import freenet.io.comm.NotConnectedException; import freenet.io.comm.Peer; import freenet.io.comm.PeerParseException; import freenet.io.comm.ReferenceSignatureVerificationException; import freenet.io.comm.RetrievalException; import freenet.io.xfer.BulkReceiver; import freenet.io.xfer.BulkTransmitter; import freenet.io.xfer.PartiallyReceivedBulk; import freenet.keys.FreenetURI; import freenet.l10n.NodeL10n; import freenet.node.useralerts.AbstractUserAlert; import freenet.node.useralerts.BookmarkFeedUserAlert; import freenet.node.useralerts.DownloadFeedUserAlert; import freenet.node.useralerts.N2NTMUserAlert; import freenet.node.useralerts.UserAlert; import freenet.support.Base64; import freenet.support.HTMLNode; import freenet.support.IllegalBase64Exception; import freenet.support.Logger; import freenet.support.SimpleFieldSet; import freenet.support.SizeUtil; import freenet.support.api.HTTPUploadedFile; import freenet.support.api.RandomAccessBuffer; import freenet.support.io.BucketTools; import freenet.support.io.ByteArrayRandomAccessBuffer; import freenet.support.io.FileRandomAccessBuffer; import freenet.support.io.FileUtil; import static java.util.concurrent.TimeUnit.DAYS; public class DarknetPeerNode extends PeerNode { /** Name of this node */ String myName; /** True if this peer is not to be connected with */ private boolean isDisabled; /** True if we don't send handshake requests to this peer, but will connect if we receive one */ private boolean isListenOnly; /** True if we send handshake requests to this peer in infrequent bursts */ private boolean isBurstOnly; /** True if we want to ignore the source port of the node's sent packets. * This is normally set when dealing with an Evil Corporate Firewall which rewrites the port on outgoing * packets but does not redirect incoming packets destined to the rewritten port. * What it does is this: If we have an address with the same IP but a different port, to the detectedPeer, * we use that instead. */ private boolean ignoreSourcePort; /** True if we want to allow LAN/localhost addresses. */ private boolean allowLocalAddresses; /** Extra peer data file numbers */ private LinkedHashSet<Integer> extraPeerDataFileNumbers; /** Private comment on the peer for /friends/ page */ private String privateDarknetComment; /** Private comment on the peer for /friends/ page's extra peer data file number */ private int privateDarknetCommentFileNumber; /** Queued-to-send N2NM extra peer data file numbers */ private LinkedHashSet<Integer> queuedToSendN2NMExtraPeerDataFileNumbers; private FRIEND_TRUST trustLevel; private FRIEND_VISIBILITY ourVisibility; private FRIEND_VISIBILITY theirVisibility; private static volatile boolean logMINOR; static { Logger.registerClass(DarknetPeerNode.class); } public enum FRIEND_TRUST { LOW, NORMAL, HIGH; private static final FRIEND_TRUST[] valuesBackwards; static { final FRIEND_TRUST[] values = values(); valuesBackwards = new FRIEND_TRUST[values.length]; for(int i=0;i<values.length;i++) valuesBackwards[i] = values[values.length-i-1]; } public static FRIEND_TRUST[] valuesBackwards() { return valuesBackwards.clone(); } public boolean isDefaultValue() { return equals(FRIEND_TRUST.NORMAL); } } public enum FRIEND_VISIBILITY { YES((short)0), // Visible NAME_ONLY((short)1), // Only the name is visible, but other friends can ask for a connection NO((short)2); // Not visible to our other friends at all /** The codes are persistent and used to communicate between nodes, so they must not change. * Which is why we are not using ordinal(). */ final short code; FRIEND_VISIBILITY(short code) { this.code = code; } public boolean isStricterThan(FRIEND_VISIBILITY theirVisibility) { if(theirVisibility == null) return true; // Higher number = more strict. return theirVisibility.code < code; } public static FRIEND_VISIBILITY getByCode(short code) { for(FRIEND_VISIBILITY f : values()) { if(f.code == code) return f; } return null; } public boolean isDefaultValue() { return equals(FRIEND_VISIBILITY.YES); } } /** * Create a darknet PeerNode from a SimpleFieldSet * @param fs The SimpleFieldSet to parse * @param node2 The running Node we are part of. * @param trust If this is a new node, we will use this parameter to set the initial trust level. * @throws PeerTooOldException */ public DarknetPeerNode(SimpleFieldSet fs, Node node2, NodeCrypto crypto, boolean fromLocal, FRIEND_TRUST trust, FRIEND_VISIBILITY visibility2) throws FSParseException, PeerParseException, ReferenceSignatureVerificationException, PeerTooOldException { super(fs, node2, crypto, fromLocal); String name = fs.get("myName"); if(name == null) throw new FSParseException("No name"); myName = name; if(fromLocal) { SimpleFieldSet metadata = fs.subset("metadata"); isDisabled = metadata.getBoolean("isDisabled", false); isListenOnly = metadata.getBoolean("isListenOnly", false); isBurstOnly = metadata.getBoolean("isBurstOnly", false); disableRouting = disableRoutingHasBeenSetLocally = metadata.getBoolean("disableRoutingHasBeenSetLocally", false); ignoreSourcePort = metadata.getBoolean("ignoreSourcePort", false); allowLocalAddresses = metadata.getBoolean("allowLocalAddresses", false); String s = metadata.get("trustLevel"); if(s != null) { trustLevel = FRIEND_TRUST.valueOf(s); } else { trustLevel = node.securityLevels.getDefaultFriendTrust(); System.err.println("Assuming friend ("+name+") trust is opposite of friend seclevel: "+trustLevel); } s = metadata.get("ourVisibility"); if(s != null) { ourVisibility = FRIEND_VISIBILITY.valueOf(s); } else { System.err.println("Assuming friend ("+name+") wants to be invisible"); node.createVisibilityAlert(); ourVisibility = FRIEND_VISIBILITY.NO; } s = metadata.get("theirVisibility"); if(s != null) { theirVisibility = FRIEND_VISIBILITY.valueOf(s); } else { theirVisibility = FRIEND_VISIBILITY.NO; } } else { if(trust == null) throw new IllegalArgumentException(); trustLevel = trust; ourVisibility = visibility2; } // Setup the private darknet comment note privateDarknetComment = ""; privateDarknetCommentFileNumber = -1; // Setup the extraPeerDataFileNumbers extraPeerDataFileNumbers = new LinkedHashSet<Integer>(); // Setup the queuedToSendN2NMExtraPeerDataFileNumbers queuedToSendN2NMExtraPeerDataFileNumbers = new LinkedHashSet<Integer>(); } /** * * Normally this is the address that packets have been received from from this node. * However, if ignoreSourcePort is set, we will search for a similar address with a different port * number in the node reference. */ @Override public synchronized Peer getPeer(){ Peer detectedPeer = super.getPeer(); if(ignoreSourcePort) { FreenetInetAddress addr = detectedPeer == null ? null : detectedPeer.getFreenetAddress(); int port = detectedPeer == null ? -1 : detectedPeer.getPort(); if(nominalPeer == null) return detectedPeer; for(Peer p : nominalPeer) { if(p.getPort() != port && p.getFreenetAddress().equals(addr)) { return p; } } } return detectedPeer; } /** * @return True, if we are disconnected and it has been a * sufficient time period since we last sent a handshake * attempt. */ @Override public boolean shouldSendHandshake() { synchronized(this) { if(isDisabled) return false; if(isListenOnly) return false; if(!super.shouldSendHandshake()) return false; } return true; } @Override protected synchronized boolean innerProcessNewNoderef(SimpleFieldSet fs, boolean forARK, boolean forDiffNodeRef, boolean forFullNodeRef) throws FSParseException { boolean changedAnything = super.innerProcessNewNoderef(fs, forARK, forDiffNodeRef, forFullNodeRef); String name = fs.get("myName"); if(name == null && forFullNodeRef) throw new FSParseException("No name in full noderef"); if(name != null && !name.equals(myName)) { changedAnything = true; myName = name; } return changedAnything; } @Override public synchronized SimpleFieldSet exportFieldSet() { SimpleFieldSet fs = super.exportFieldSet(); fs.putSingle("myName", getName()); return fs; } @Override public synchronized SimpleFieldSet exportMetadataFieldSet(long now) { SimpleFieldSet fs = super.exportMetadataFieldSet(now); if(isDisabled) fs.putSingle("isDisabled", "true"); if(isListenOnly) fs.putSingle("isListenOnly", "true"); if(isBurstOnly) fs.putSingle("isBurstOnly", "true"); if(ignoreSourcePort) fs.putSingle("ignoreSourcePort", "true"); if(allowLocalAddresses) fs.putSingle("allowLocalAddresses", "true"); if(disableRoutingHasBeenSetLocally) fs.putSingle("disableRoutingHasBeenSetLocally", "true"); fs.putSingle("trustLevel", trustLevel.name()); fs.putSingle("ourVisibility", ourVisibility.name()); if(theirVisibility != null) fs.putSingle("theirVisibility", theirVisibility.name()); return fs; } public synchronized String getName() { return myName; } @Override protected synchronized int getPeerNodeStatus(long now, long backedOffUntilRT, long backedOffUntilBulk, boolean overPingThreshold, boolean noLoadStats) { if(isDisabled) { return PeerManager.PEER_NODE_STATUS_DISABLED; } int status = super.getPeerNodeStatus(now, backedOffUntilRT, backedOffUntilBulk, overPingThreshold, noLoadStats); if(status == PeerManager.PEER_NODE_STATUS_CONNECTED || status == PeerManager.PEER_NODE_STATUS_CLOCK_PROBLEM || status == PeerManager.PEER_NODE_STATUS_ROUTING_BACKED_OFF || status == PeerManager.PEER_NODE_STATUS_CONN_ERROR || status == PeerManager.PEER_NODE_STATUS_TOO_NEW || status == PeerManager.PEER_NODE_STATUS_TOO_OLD || status == PeerManager.PEER_NODE_STATUS_ROUTING_DISABLED || status == PeerManager.PEER_NODE_STATUS_DISCONNECTING || status == PeerManager.PEER_NODE_STATUS_NO_LOAD_STATS) return status; if(isListenOnly) return PeerManager.PEER_NODE_STATUS_LISTEN_ONLY; if(isBurstOnly) return PeerManager.PEER_NODE_STATUS_LISTENING; return status; } public void enablePeer() { synchronized(this) { isDisabled = false; } setPeerNodeStatus(System.currentTimeMillis()); node.peers.writePeersDarknetUrgent(); } public void disablePeer() { synchronized(this) { isDisabled = true; } if(isConnected()) { forceDisconnect(); } stopARKFetcher(); setPeerNodeStatus(System.currentTimeMillis()); node.peers.writePeersDarknetUrgent(); } @Override public synchronized boolean isDisabled() { return isDisabled; } public void setListenOnly(boolean setting) { synchronized(this) { isListenOnly = setting; } if(setting && isBurstOnly()) { setBurstOnly(false); } if(setting) { stopARKFetcher(); } setPeerNodeStatus(System.currentTimeMillis()); node.peers.writePeersDarknetUrgent(); } public synchronized boolean isListenOnly() { return isListenOnly; } public void setBurstOnly(boolean setting) { synchronized(this) { isBurstOnly = setting; } if(setting && isListenOnly()) { setListenOnly(false); } long now = System.currentTimeMillis(); if(!setting) { synchronized(this) { sendHandshakeTime = now; // don't keep any long handshake delays we might have had under BurstOnly } } setPeerNodeStatus(now); node.peers.writePeersDarknetUrgent(); } public void setIgnoreSourcePort(boolean setting) { synchronized(this) { ignoreSourcePort = setting; } } /** * Change the routing status of a peer * * @param shouldRoute * @param localRequest (true everywhere but in NodeDispatcher) */ public void setRoutingStatus(boolean shouldRoute, boolean localRequest) { synchronized(this) { if(localRequest) disableRoutingHasBeenSetLocally = !shouldRoute; else disableRoutingHasBeenSetRemotely = !shouldRoute; disableRouting = disableRoutingHasBeenSetLocally || disableRoutingHasBeenSetRemotely; } if(localRequest) { Message msg = DMT.createRoutingStatus(shouldRoute); try { sendAsync(msg, null, node.nodeStats.setRoutingStatusCtr); } catch(NotConnectedException e) { // ok } } setPeerNodeStatus(System.currentTimeMillis()); node.peers.writePeersDarknetUrgent(); } @Override public boolean isIgnoreSource() { return ignoreSourcePort; } @Override public boolean isBurstOnly() { synchronized(this) { if(isBurstOnly) return true; } return super.isBurstOnly(); } @Override public boolean allowLocalAddresses() { synchronized(this) { if(allowLocalAddresses) return true; } return super.allowLocalAddresses(); } public void setAllowLocalAddresses(boolean setting) { synchronized(this) { allowLocalAddresses = setting; } node.peers.writePeersDarknetUrgent(); } public boolean readExtraPeerData() { String extraPeerDataDirPath = node.getExtraPeerDataDir(); File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString()); if(!extraPeerDataPeerDir.exists()) { return false; } if(!extraPeerDataPeerDir.isDirectory()) { Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath()); return false; } File[] extraPeerDataFiles = extraPeerDataPeerDir.listFiles(); if(extraPeerDataFiles == null) { return false; } boolean gotError = false; boolean readResult = false; for (File extraPeerDataFile : extraPeerDataFiles) { Integer fileNumber; try { fileNumber = Integer.valueOf(extraPeerDataFile.getName()); } catch (NumberFormatException e) { gotError = true; continue; } synchronized(extraPeerDataFileNumbers) { extraPeerDataFileNumbers.add(fileNumber); } readResult = readExtraPeerDataFile(extraPeerDataFile, fileNumber.intValue()); if(!readResult) { gotError = true; } } return !gotError; } public boolean rereadExtraPeerDataFile(int fileNumber) { if(logMINOR) Logger.minor(this, "Rereading peer data file "+fileNumber+" for "+shortToString()); String extraPeerDataDirPath = node.getExtraPeerDataDir(); File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString()); if(!extraPeerDataPeerDir.exists()) { Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath()); return false; } if(!extraPeerDataPeerDir.isDirectory()) { Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath()); return false; } File extraPeerDataFile = new File(extraPeerDataDirPath+File.separator+getIdentityString()+File.separator+fileNumber); if(!extraPeerDataFile.exists()) { Logger.error(this, "Extra peer data file for peer does not exist: "+extraPeerDataFile.getPath()); return false; } return readExtraPeerDataFile(extraPeerDataFile, fileNumber); } public boolean readExtraPeerDataFile(File extraPeerDataFile, int fileNumber) { if(logMINOR) Logger.minor(this, "Reading "+extraPeerDataFile+" : "+fileNumber+" for "+shortToString()); boolean gotError = false; if(!extraPeerDataFile.exists()) { if(logMINOR) Logger.minor(this, "Does not exist"); return false; } Logger.normal(this, "extraPeerDataFile: "+extraPeerDataFile.getPath()); FileInputStream fis; try { fis = new FileInputStream(extraPeerDataFile); } catch (FileNotFoundException e1) { Logger.normal(this, "Extra peer data file not found: "+extraPeerDataFile.getPath()); return false; } InputStreamReader isr; try { isr = new InputStreamReader(fis, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e); } BufferedReader br = new BufferedReader(isr); SimpleFieldSet fs = null; try { // Read in the single SimpleFieldSet fs = new SimpleFieldSet(br, false, true); } catch (EOFException e3) { // End of file, fine } catch (IOException e4) { Logger.error(this, "Could not read extra peer data file: "+e4, e4); } finally { try { br.close(); } catch (IOException e5) { Logger.error(this, "Ignoring "+e5+" caught reading "+extraPeerDataFile.getPath(), e5); } } if(fs == null) { Logger.normal(this, "Deleting corrupt (too short?) file: "+extraPeerDataFile); deleteExtraPeerDataFile(fileNumber); return true; } boolean parseResult = false; try { parseResult = parseExtraPeerData(fs, extraPeerDataFile, fileNumber); if(!parseResult) { gotError = true; } } catch (FSParseException e2) { Logger.error(this, "Could not parse extra peer data: "+e2+ '\n' +fs.toString(),e2); gotError = true; } return !gotError; } private boolean parseExtraPeerData(SimpleFieldSet fs, File extraPeerDataFile, int fileNumber) throws FSParseException { String extraPeerDataTypeString = fs.get("extraPeerDataType"); int extraPeerDataType = -1; try { extraPeerDataType = Integer.parseInt(extraPeerDataTypeString); } catch (NumberFormatException e) { Logger.error(this, "NumberFormatException parsing extraPeerDataType ("+extraPeerDataTypeString+") in file "+extraPeerDataFile.getPath()); return false; } if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_N2NTM) { node.handleNodeToNodeTextMessageSimpleFieldSet(fs, this, fileNumber); return true; } else if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_PEER_NOTE) { String peerNoteTypeString = fs.get("peerNoteType"); int peerNoteType = -1; try { peerNoteType = Integer.parseInt(peerNoteTypeString); } catch (NumberFormatException e) { Logger.error(this, "NumberFormatException parsing peerNoteType ("+peerNoteTypeString+") in file "+extraPeerDataFile.getPath()); return false; } if(peerNoteType == Node.PEER_NOTE_TYPE_PRIVATE_DARKNET_COMMENT) { synchronized(this) { try { privateDarknetComment = Base64.decodeUTF8(fs.get("privateDarknetComment")); } catch (IllegalBase64Exception e) { Logger.error(this, "Bad Base64 encoding when decoding a private darknet comment SimpleFieldSet", e); return false; } privateDarknetCommentFileNumber = fileNumber; } return true; } Logger.error(this, "Read unknown peer note type '"+peerNoteType+"' from file "+extraPeerDataFile.getPath()); return false; } else if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_QUEUED_TO_SEND_N2NM) { boolean sendSuccess = false; int type = fs.getInt("n2nType"); if(isConnected()) { Message n2nm; if(fs.get("extraPeerDataType") != null) { fs.removeValue("extraPeerDataType"); } if(fs.get("senderFileNumber") != null) { fs.removeValue("senderFileNumber"); } fs.putOverwrite("senderFileNumber", String.valueOf(fileNumber)); if(fs.get("sentTime") != null) { fs.removeValue("sentTime"); } fs.putOverwrite("sentTime", Long.toString(System.currentTimeMillis())); try { n2nm = DMT.createNodeToNodeMessage(type, fs.toString().getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { Logger.error(this, "UnsupportedEncodingException processing extraPeerDataType ("+extraPeerDataTypeString+") in file "+extraPeerDataFile.getPath(), e); throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e); } try { synchronized(queuedToSendN2NMExtraPeerDataFileNumbers) { node.usm.send(this, n2nm, null); Logger.normal(this, "Sent queued ("+fileNumber+") N2NM to '"+getName()+"': "+n2nm); sendSuccess = true; queuedToSendN2NMExtraPeerDataFileNumbers.remove(fileNumber); } deleteExtraPeerDataFile(fileNumber); } catch (NotConnectedException e) { sendSuccess = false; // redundant, but clear } } if(!sendSuccess) { synchronized(queuedToSendN2NMExtraPeerDataFileNumbers) { fs.putOverwrite("extraPeerDataType", Integer.toString(extraPeerDataType)); fs.removeValue("sentTime"); queuedToSendN2NMExtraPeerDataFileNumbers.add(Integer.valueOf(fileNumber)); } } return true; } else if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_BOOKMARK) { Logger.normal(this, "Read friend bookmark" + fs.toString()); handleFproxyBookmarkFeed(fs, fileNumber); return true; } else if(extraPeerDataType == Node.EXTRA_PEER_DATA_TYPE_DOWNLOAD) { Logger.normal(this, "Read friend download" + fs.toString()); handleFproxyDownloadFeed(fs, fileNumber); return true; } Logger.error(this, "Read unknown extra peer data type '"+extraPeerDataType+"' from file "+extraPeerDataFile.getPath()); return false; } public int writeNewExtraPeerDataFile(SimpleFieldSet fs, int extraPeerDataType) { String extraPeerDataDirPath = node.getExtraPeerDataDir(); if(extraPeerDataType > 0) fs.putOverwrite("extraPeerDataType", Integer.toString(extraPeerDataType)); File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString()); if(!extraPeerDataPeerDir.exists()) { if(!extraPeerDataPeerDir.mkdir()) { Logger.error(this, "Extra peer data directory for peer could not be created: "+extraPeerDataPeerDir.getPath()); return -1; } } if(!extraPeerDataPeerDir.isDirectory()) { Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath()); return -1; } Integer[] localFileNumbers; int nextFileNumber = 0; synchronized(extraPeerDataFileNumbers) { // Find the first free slot localFileNumbers = extraPeerDataFileNumbers.toArray(new Integer[extraPeerDataFileNumbers.size()]); Arrays.sort(localFileNumbers); for (int localFileNumber : localFileNumbers) { if(localFileNumber > nextFileNumber) { break; } nextFileNumber = localFileNumber + 1; } extraPeerDataFileNumbers.add(nextFileNumber); } FileOutputStream fos; File extraPeerDataFile = new File(extraPeerDataPeerDir.getPath()+File.separator+nextFileNumber); if(extraPeerDataFile.exists()) { Logger.error(this, "Extra peer data file already exists: "+extraPeerDataFile.getPath()); return -1; } String f = extraPeerDataFile.getPath(); try { fos = new FileOutputStream(f); } catch (FileNotFoundException e2) { Logger.error(this, "Cannot write extra peer data file to disk: Cannot create " + f + " - " + e2, e2); return -1; } OutputStreamWriter w; try { w = new OutputStreamWriter(fos, "UTF-8"); } catch (UnsupportedEncodingException e2) { throw new Error("Impossible: JVM doesn't support UTF-8: " + e2, e2); } BufferedWriter bw = new BufferedWriter(w); try { fs.writeTo(bw); bw.close(); } catch (IOException e) { try { fos.close(); } catch (IOException e1) { Logger.error(this, "Cannot close extra peer data file: "+e, e); } Logger.error(this, "Cannot write file: " + e, e); return -1; } return nextFileNumber; } public void deleteExtraPeerDataFile(int fileNumber) { String extraPeerDataDirPath = node.getExtraPeerDataDir(); File extraPeerDataPeerDir = new File(extraPeerDataDirPath, getIdentityString()); if(!extraPeerDataPeerDir.exists()) { Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath()); return; } if(!extraPeerDataPeerDir.isDirectory()) { Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath()); return; } File extraPeerDataFile = new File(extraPeerDataPeerDir, Integer.toString(fileNumber)); if(!extraPeerDataFile.exists()) { Logger.error(this, "Extra peer data file for peer does not exist: "+extraPeerDataFile.getPath()); return; } synchronized(extraPeerDataFileNumbers) { extraPeerDataFileNumbers.remove(fileNumber); } if(!extraPeerDataFile.delete()) { if(extraPeerDataFile.exists()) { Logger.error(this, "Cannot delete file "+extraPeerDataFile+" after sending message to "+getPeer()+" - it may be resent on resting the node"); } else { Logger.normal(this, "File does not exist when deleting: "+extraPeerDataFile+" after sending message to "+getPeer()); } } } public void removeExtraPeerDataDir() { String extraPeerDataDirPath = node.getExtraPeerDataDir(); File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString()); if(!extraPeerDataPeerDir.exists()) { Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath()); return; } if(!extraPeerDataPeerDir.isDirectory()) { Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath()); return; } Integer[] localFileNumbers; synchronized(extraPeerDataFileNumbers) { localFileNumbers = extraPeerDataFileNumbers.toArray(new Integer[extraPeerDataFileNumbers.size()]); } for (Integer localFileNumber : localFileNumbers) { deleteExtraPeerDataFile(localFileNumber.intValue()); } extraPeerDataPeerDir.delete(); } public boolean rewriteExtraPeerDataFile(SimpleFieldSet fs, int extraPeerDataType, int fileNumber) { String extraPeerDataDirPath = node.getExtraPeerDataDir(); if(extraPeerDataType > 0) fs.putOverwrite("extraPeerDataType", Integer.toString(extraPeerDataType)); File extraPeerDataPeerDir = new File(extraPeerDataDirPath+File.separator+getIdentityString()); if(!extraPeerDataPeerDir.exists()) { Logger.error(this, "Extra peer data directory for peer does not exist: "+extraPeerDataPeerDir.getPath()); return false; } if(!extraPeerDataPeerDir.isDirectory()) { Logger.error(this, "Extra peer data directory for peer not a directory: "+extraPeerDataPeerDir.getPath()); return false; } File extraPeerDataFile = new File(extraPeerDataDirPath+File.separator+getIdentityString()+File.separator+fileNumber); if(!extraPeerDataFile.exists()) { Logger.error(this, "Extra peer data file for peer does not exist: "+extraPeerDataFile.getPath()); return false; } String f = extraPeerDataFile.getPath(); FileOutputStream fos; try { fos = new FileOutputStream(f); } catch (FileNotFoundException e2) { Logger.error(this, "Cannot write extra peer data file to disk: Cannot open " + f + " - " + e2, e2); return false; } OutputStreamWriter w; try { w = new OutputStreamWriter(fos, "UTF-8"); } catch (UnsupportedEncodingException e2) { throw new Error("Impossible: JVM doesn't support UTF-8: " + e2, e2); } BufferedWriter bw = new BufferedWriter(w); try { fs.writeTo(bw); bw.close(); } catch (IOException e) { try { fos.close(); } catch (IOException e1) { Logger.error(this, "Cannot close extra peer data file: "+e, e); } Logger.error(this, "Cannot write file: " + e, e); return false; } return true; } public synchronized String getPrivateDarknetCommentNote() { return privateDarknetComment; } public synchronized void setPrivateDarknetCommentNote(String comment) { int localFileNumber; privateDarknetComment = comment; localFileNumber = privateDarknetCommentFileNumber; SimpleFieldSet fs = new SimpleFieldSet(true); fs.put("peerNoteType", Node.PEER_NOTE_TYPE_PRIVATE_DARKNET_COMMENT); fs.putSingle("privateDarknetComment", Base64.encodeUTF8(comment)); if(localFileNumber == -1) { localFileNumber = writeNewExtraPeerDataFile(fs, Node.EXTRA_PEER_DATA_TYPE_PEER_NOTE); privateDarknetCommentFileNumber = localFileNumber; } else { rewriteExtraPeerDataFile(fs, Node.EXTRA_PEER_DATA_TYPE_PEER_NOTE, localFileNumber); } } @Override public void queueN2NM(SimpleFieldSet fs) { int fileNumber = writeNewExtraPeerDataFile( fs, Node.EXTRA_PEER_DATA_TYPE_QUEUED_TO_SEND_N2NM); synchronized(queuedToSendN2NMExtraPeerDataFileNumbers) { queuedToSendN2NMExtraPeerDataFileNumbers.add(fileNumber); } } public void sendQueuedN2NMs() { if(logMINOR) Logger.minor(this, "Sending queued N2NMs for "+shortToString()); Integer[] localFileNumbers; synchronized(queuedToSendN2NMExtraPeerDataFileNumbers) { localFileNumbers = queuedToSendN2NMExtraPeerDataFileNumbers.toArray(new Integer[queuedToSendN2NMExtraPeerDataFileNumbers.size()]); } Arrays.sort(localFileNumbers); for (Integer localFileNumber : localFileNumbers) { rereadExtraPeerDataFile(localFileNumber.intValue()); } } @Override void startARKFetcher() { synchronized(this) { if(isListenOnly) { Logger.minor(this, "Not starting ark fetcher for "+this+" as it's in listen-only mode."); return; } } super.startARKFetcher(); } @Override public String getTMCIPeerInfo() { return getName()+'\t'+super.getTMCIPeerInfo(); } /** * A method to be called once at the beginning of every time isConnected() is true */ @Override protected void onConnect() { super.onConnect(); sendQueuedN2NMs(); } // File transfer offers // FIXME this should probably be somewhere else, along with the N2NM stuff... but where? // FIXME this should be persistent across node restarts /** Files I have offered to this peer */ private final HashMap<Long, FileOffer> myFileOffersByUID = new HashMap<Long, FileOffer>(); /** Files this peer has offered to me */ private final HashMap<Long, FileOffer> hisFileOffersByUID = new HashMap<Long, FileOffer>(); private void storeOffers() { // FIXME do something } // FIXME refactor this. We want to be able to send file transfers from code that isn't related to fproxy. // FIXME and it should be able to talk to plugins on other nodes etc etc. // FIXME there are already type fields etc, so this shouldn't be too difficult? But it's not really supported at the moment. // FIXME See also e.g. fcp/SendTextMessage. class FileOffer { final long uid; final String filename; final String mimeType; final String comment; /** Only valid if amIOffering == false. Set when start receiving. */ private File destination; private RandomAccessBuffer data; final long size; /** Who is offering it? True = I am offering it, False = I am being offered it */ final boolean amIOffering; private PartiallyReceivedBulk prb; private BulkTransmitter transmitter; private BulkReceiver receiver; /** True if the offer has either been accepted or rejected */ private boolean acceptedOrRejected; FileOffer(long uid, RandomAccessBuffer data, String filename, String mimeType, String comment) throws IOException { this.uid = uid; this.data = data; this.filename = filename; this.mimeType = mimeType; this.comment = comment; size = data.size(); amIOffering = true; } public FileOffer(SimpleFieldSet fs, boolean amIOffering) throws FSParseException { uid = fs.getLong("uid"); size = fs.getLong("size"); mimeType = fs.get("metadata.contentType"); filename = FileUtil.sanitize(fs.get("filename"), mimeType); destination = null; String s = fs.get("comment"); if(s != null) { try { s = Base64.decodeUTF8(s); } catch (IllegalBase64Exception e) { // Maybe it wasn't encoded? FIXME remove Logger.error(this, "Bad Base64 encoding when decoding a private darknet comment SimpleFieldSet", e); } } comment = s; this.amIOffering = amIOffering; } public void toFieldSet(SimpleFieldSet fs) { fs.put("uid", uid); fs.putSingle("filename", filename); fs.putSingle("metadata.contentType", mimeType); fs.putSingle("comment", Base64.encodeUTF8(comment)); fs.put("size", size); } public void accept() { acceptedOrRejected = true; final String baseFilename = "direct-"+FileUtil.sanitize(getName())+"-"+filename; final File dest = node.clientCore.downloadsDir().file(baseFilename+".part"); destination = node.clientCore.downloadsDir().file(baseFilename); try { data = new FileRandomAccessBuffer(dest, size, false); } catch (IOException e) { // Impossible throw new Error("Impossible: FileNotFoundException opening with RAF with rw! "+e, e); } prb = new PartiallyReceivedBulk(node.usm, size, Node.PACKET_SIZE, data, false); receiver = new BulkReceiver(prb, DarknetPeerNode.this, uid, null); // FIXME make this persistent node.executor.execute(new Runnable() { @Override public void run() { if(logMINOR) Logger.minor(this, "Received file"); try { if(!receiver.receive()) { String err = "Failed to receive "+this; Logger.error(this, err); System.err.println(err); onReceiveFailure(); } else { data.close(); if(!dest.renameTo(node.clientCore.downloadsDir().file(baseFilename))){ Logger.error(this, "Failed to rename "+dest.getName()+" to remove .part suffix."); } onReceiveSuccess(); } } catch (Throwable t) { Logger.error(this, "Caught "+t+" receiving file", t); onReceiveFailure(); } finally { remove(); } if(logMINOR) Logger.minor(this, "Received file"); } }, "Receiver for bulk transfer "+uid+":"+filename); sendFileOfferAccepted(uid); } protected void remove() { Long l = uid; synchronized(DarknetPeerNode.this) { myFileOffersByUID.remove(l); hisFileOffersByUID.remove(l); } data.close(); } public void send() throws DisconnectedException { prb = new PartiallyReceivedBulk(node.usm, size, Node.PACKET_SIZE, data, true); transmitter = new BulkTransmitter(prb, DarknetPeerNode.this, uid, false, node.nodeStats.nodeToNodeCounter, false); if(logMINOR) Logger.minor(this, "Sending "+uid); node.executor.execute(new Runnable() { @Override public void run() { if(logMINOR) Logger.minor(this, "Sending file"); try { if(!transmitter.send()) { String err = "Failed to send "+uid+" for "+FileOffer.this; Logger.error(this, err); System.err.println(err); } } catch (Throwable t) { Logger.error(this, "Caught "+t+" sending file", t); remove(); } if(logMINOR) Logger.minor(this, "Sent file"); } }, "Sender for bulk transfer "+uid+":"+filename); } public void reject() { acceptedOrRejected = true; sendFileOfferRejected(uid); } public void onRejected() { transmitter.cancel("FileOffer: Offer rejected"); // FIXME prb's can't be shared, right? Well they aren't here... prb.abort(RetrievalException.CANCELLED_BY_RECEIVER, "Cancelled by receiver"); } protected void onReceiveFailure() { UserAlert alert = new AbstractUserAlert() { @Override public String dismissButtonText() { return NodeL10n.getBase().getString("UserAlert.hide"); } @Override public HTMLNode getHTMLText() { HTMLNode div = new HTMLNode("div"); div.addChild("p", l10n("failedReceiveHeader", new String[] { "filename", "node" }, new String[] { filename, getName() })); // Descriptive table describeFile(div); return div; } @Override public short getPriorityClass() { return UserAlert.MINOR; } @Override public String getText() { StringBuilder sb = new StringBuilder(); sb.append(l10n("failedReceiveHeader", new String[] { "filename", "node" }, new String[] { filename, getName() })); sb.append('\n'); sb.append(l10n("fileLabel")); sb.append(' '); sb.append(filename); sb.append('\n'); sb.append(l10n("sizeLabel")); sb.append(' '); sb.append(SizeUtil.formatSize(size)); sb.append('\n'); sb.append(l10n("mimeLabel")); sb.append(' '); sb.append(mimeType); sb.append('\n'); sb.append(l10n("senderLabel")); sb.append(' '); sb.append(getName()); sb.append('\n'); if(comment != null && comment.length() > 0) { sb.append(l10n("commentLabel")); sb.append(' '); sb.append(comment); } return sb.toString(); } @Override public String getTitle() { return l10n("failedReceiveTitle"); } @Override public boolean isValid() { return true; } @Override public void isValid(boolean validity) { // Ignore } @Override public void onDismiss() { // Ignore } @Override public boolean shouldUnregisterOnDismiss() { return true; } @Override public boolean userCanDismiss() { return true; } @Override public String getShortText() { return l10n("failedReceiveShort", new String[] { "filename", "node" }, new String[] { filename, getName() }); } }; node.clientCore.alerts.register(alert); } private void onReceiveSuccess() { UserAlert alert = new AbstractUserAlert() { @Override public String dismissButtonText() { return NodeL10n.getBase().getString("UserAlert.hide"); } @Override public HTMLNode getHTMLText() { HTMLNode div = new HTMLNode("div"); // FIXME localise!!! div.addChild("p", l10n("succeededReceiveHeader", new String[] { "filename", "node" }, new String[] { filename, getName() })); // Descriptive table describeFile(div); return div; } @Override public short getPriorityClass() { return UserAlert.MINOR; } @Override public String getText() { String header = l10n("succeededReceiveHeader", new String[] { "filename", "node" }, new String[] { filename, getName() }); return describeFileText(header); } @Override public String getTitle() { return l10n("succeededReceiveTitle"); } @Override public boolean isValid() { return true; } @Override public void isValid(boolean validity) { // Ignore } @Override public void onDismiss() { // Ignore } @Override public boolean shouldUnregisterOnDismiss() { return true; } @Override public boolean userCanDismiss() { return true; } @Override public String getShortText() { return l10n("succeededReceiveShort", new String[] { "filename", "node" }, new String[] { filename, getName() }); } }; node.clientCore.alerts.register(alert); } /** Ask the user whether (s)he wants to download a file from a direct peer */ public UserAlert askUserUserAlert() { return new AbstractUserAlert() { @Override public String dismissButtonText() { return null; // Cannot hide, but can reject } @Override public HTMLNode getHTMLText() { HTMLNode div = new HTMLNode("div"); div.addChild("p", l10n("offeredFileHeader", "name", getName())); // Descriptive table describeFile(div); // Accept/reject form // Hopefully we will have a container when this function is called! HTMLNode form = node.clientCore.getToadletContainer().addFormChild(div, "/friends/", "f2fFileOfferAcceptForm"); // FIXME node_ is inefficient form.addChild("input", new String[] { "type", "name" }, new String[] { "hidden", "node_"+DarknetPeerNode.this.hashCode() }); form.addChild("input", new String[] { "type", "name", "value" }, new String[] { "hidden", "id", Long.toString(uid) }); form.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "acceptTransfer", l10n("acceptTransferButton") }); form.addChild("input", new String[] { "type", "name", "value" }, new String[] { "submit", "rejectTransfer", l10n("rejectTransferButton") }); return div; } @Override public short getPriorityClass() { return UserAlert.MINOR; } @Override public String getText() { String header = l10n("offeredFileHeader", "name", getName()); return describeFileText(header); } @Override public String getTitle() { return l10n("askUserTitle"); } @Override public boolean isValid() { if(acceptedOrRejected) { node.clientCore.alerts.unregister(this); return false; } return true; } @Override public void isValid(boolean validity) { // Ignore } @Override public void onDismiss() { // Ignore } @Override public boolean shouldUnregisterOnDismiss() { return false; } @Override public boolean userCanDismiss() { return false; // should accept or reject } @Override public String getShortText() { return l10n("offeredFileShort", new String[] { "filename", "node" }, new String[] { filename, getName() }); } }; } protected void addComment(HTMLNode node) { String[] lines = comment.split("\n"); for (int i = 0, c = lines.length; i < c; i++) { node.addChild("#", lines[i]); if(i != lines.length - 1) node.addChild("br"); } } private String l10n(String key) { return NodeL10n.getBase().getString("FileOffer."+key); } private String l10n(String key, String pattern, String value) { return NodeL10n.getBase().getString("FileOffer."+key, pattern, value); } private String l10n(String key, String[] pattern, String[] value) { return NodeL10n.getBase().getString("FileOffer."+key, pattern, value); } private String describeFileText(String header) { StringBuilder sb = new StringBuilder(); sb.append(header); sb.append('\n'); sb.append(l10n("fileLabel")); sb.append(' '); sb.append(filename); sb.append('\n'); sb.append(l10n("sizeLabel")); sb.append(' '); sb.append(SizeUtil.formatSize(size)); sb.append('\n'); sb.append(l10n("mimeLabel")); sb.append(' '); sb.append(mimeType); sb.append('\n'); sb.append(l10n("senderLabel")); sb.append(' '); sb.append(userToString()); sb.append('\n'); if(comment != null && comment.length() > 0) { sb.append(l10n("commentLabel")); sb.append(' '); sb.append(comment); } return sb.toString(); } private void describeFile(HTMLNode div) { HTMLNode table = div.addChild("table", "border", "0"); HTMLNode row = table.addChild("tr"); row.addChild("td").addChild("#", l10n("fileLabel")); row.addChild("td").addChild("#", filename); if(destination != null) { row = table.addChild("tr"); row.addChild("td").addChild("#", l10n("fileSavedToLabel")); row.addChild("td").addChild("#", destination.getPath()); } row = table.addChild("tr"); row.addChild("td").addChild("#", l10n("sizeLabel")); row.addChild("td").addChild("#", SizeUtil.formatSize(size)); row = table.addChild("tr"); row.addChild("td").addChild("#", l10n("mimeLabel")); row.addChild("td").addChild("#", mimeType); row = table.addChild("tr"); row.addChild("td").addChild("#", l10n("senderLabel")); row.addChild("td").addChild("#", getName()); row = table.addChild("tr"); if(comment != null && comment.length() > 0) { row.addChild("td").addChild("#", l10n("commentLabel")); addComment(row.addChild("td")); } } } public int sendBookmarkFeed(FreenetURI uri, String name, String description, boolean hasAnActiveLink) { long now = System.currentTimeMillis(); SimpleFieldSet fs = new SimpleFieldSet(true); fs.putSingle("URI", uri.toString()); fs.putSingle("Name", name); fs.put("composedTime", now); fs.put("hasAnActivelink", hasAnActiveLink); if(description != null) fs.putSingle("Description", Base64.encodeUTF8(description)); fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_BOOKMARK); sendNodeToNodeMessage(fs, Node.N2N_MESSAGE_TYPE_FPROXY, true, now, true); setPeerNodeStatus(System.currentTimeMillis()); return getPeerNodeStatus(); } public int sendDownloadFeed(FreenetURI URI, String description) { long now = System.currentTimeMillis(); SimpleFieldSet fs = new SimpleFieldSet(true); fs.putSingle("URI", URI.toString()); fs.put("composedTime", now); if(description != null) { fs.putSingle("Description", Base64.encodeUTF8(description)); } fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_DOWNLOAD); sendNodeToNodeMessage(fs, Node.N2N_MESSAGE_TYPE_FPROXY, true, now, true); setPeerNodeStatus(System.currentTimeMillis()); return getPeerNodeStatus(); } public int sendTextFeed(String message) { long now = System.currentTimeMillis(); SimpleFieldSet fs = new SimpleFieldSet(true); fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_USERALERT); fs.putSingle("text", Base64.encodeUTF8(message)); fs.put("composedTime", now); sendNodeToNodeMessage(fs, Node.N2N_MESSAGE_TYPE_FPROXY, true, now, true); this.setPeerNodeStatus(System.currentTimeMillis()); return getPeerNodeStatus(); } public int sendFileOfferAccepted(long uid) { long now = System.currentTimeMillis(); storeOffers(); SimpleFieldSet fs = new SimpleFieldSet(true); fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_ACCEPTED); fs.put("uid", uid); if(logMINOR) Logger.minor(this, "Sending node to node message (file offer accepted):\n"+fs); sendNodeToNodeMessage(fs, Node.N2N_MESSAGE_TYPE_FPROXY, true, now, true); setPeerNodeStatus(System.currentTimeMillis()); return getPeerNodeStatus(); } public int sendFileOfferRejected(long uid) { long now = System.currentTimeMillis(); storeOffers(); SimpleFieldSet fs = new SimpleFieldSet(true); fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER_REJECTED); fs.put("uid", uid); if(logMINOR) Logger.minor(this, "Sending node to node message (file offer rejected):\n"+fs); sendNodeToNodeMessage(fs, Node.N2N_MESSAGE_TYPE_FPROXY, true, now, true); setPeerNodeStatus(System.currentTimeMillis()); return getPeerNodeStatus(); } private int sendFileOffer(String fnam, String mime, String message, RandomAccessBuffer data) throws IOException { long uid = node.random.nextLong(); long now = System.currentTimeMillis(); FileOffer fo = new FileOffer(uid, data, fnam, mime, message); synchronized(this) { myFileOffersByUID.put(uid, fo); } storeOffers(); SimpleFieldSet fs = new SimpleFieldSet(true); fo.toFieldSet(fs); if(logMINOR) Logger.minor(this, "Sending node to node message (file offer):\n"+fs); fs.put("type", Node.N2N_TEXT_MESSAGE_TYPE_FILE_OFFER); sendNodeToNodeMessage(fs, Node.N2N_MESSAGE_TYPE_FPROXY, true, now, true); setPeerNodeStatus(System.currentTimeMillis()); return getPeerNodeStatus(); } public int sendFileOffer(File file, String message) throws IOException { String fnam = file.getName(); String mime = DefaultMIMETypes.guessMIMEType(fnam, false); RandomAccessBuffer data = new FileRandomAccessBuffer(file, true); return sendFileOffer(fnam, mime, message, data); } public int sendFileOffer(HTTPUploadedFile file, String message) throws IOException { String fnam = file.getFilename(); String mime = file.getContentType(); RandomAccessBuffer data = new ByteArrayRandomAccessBuffer(BucketTools.toByteArray(file.getData())); return sendFileOffer(fnam, mime, message, data); } public void handleFproxyN2NTM(SimpleFieldSet fs, int fileNumber) { String text = null; long composedTime = fs.getLong("composedTime", -1); long sentTime = fs.getLong("sentTime", -1); long receivedTime = fs.getLong("receivedTime", -1); try { text = Base64.decodeUTF8(fs.get("text")); } catch (IllegalBase64Exception e) { Logger.error(this, "Bad Base64 encoding when decoding a N2NTM SimpleFieldSet", e); return; } N2NTMUserAlert userAlert = new N2NTMUserAlert(this, text, fileNumber, composedTime, sentTime, receivedTime); node.clientCore.alerts.register(userAlert); } public void handleFproxyFileOffer(SimpleFieldSet fs, int fileNumber) { final FileOffer offer; try { offer = new FileOffer(fs, false); } catch (FSParseException e) { Logger.error(this, "Could not parse offer: "+e+" on "+this+" :\n"+fs, e); return; } Long u = offer.uid; synchronized (this) { if (hisFileOffersByUID.containsKey(u)) return; // Ignore re-advertisement hisFileOffersByUID.put(u, offer); } // Don't persist for now - FIXME this.deleteExtraPeerDataFile(fileNumber); UserAlert alert = offer.askUserUserAlert(); node.clientCore.alerts.register(alert); } public void acceptTransfer(long id) { if(logMINOR) Logger.minor(this, "Accepting transfer "+id+" on "+this); FileOffer fo; synchronized(this) { fo = hisFileOffersByUID.get(id); } if(fo == null) { Logger.error(this, "Cannot accept transfer "+id+" - does not exist"); return; } fo.accept(); } public void rejectTransfer(long id) { FileOffer fo; synchronized(this) { fo = hisFileOffersByUID.remove(id); } if(fo == null) { Logger.error(this, "Cannot accept transfer "+id+" - does not exist"); return; } fo.reject(); } public void handleFproxyFileOfferAccepted(SimpleFieldSet fs, int fileNumber) { // Don't persist for now - FIXME this.deleteExtraPeerDataFile(fileNumber); long uid; try { uid = fs.getLong("uid"); } catch (FSParseException e) { Logger.error(this, "Could not parse offer accepted: "+e+" on "+this+" :\n"+fs, e); return; } if(logMINOR) Logger.minor(this, "Offer accepted for "+uid); FileOffer fo; synchronized(this) { fo = (myFileOffersByUID.get(uid)); } if(fo == null) { Logger.error(this, "No such offer: "+uid); try { sendAsync(DMT.createFNPBulkSendAborted(uid), null, node.nodeStats.nodeToNodeCounter); } catch (NotConnectedException e) { // Fine by me! } return; } try { fo.send(); } catch (DisconnectedException e) { Logger.error(this, "Cannot send because node disconnected: "+e+" for "+uid+":"+fo.filename, e); } } public void handleFproxyFileOfferRejected(SimpleFieldSet fs, int fileNumber) { // Don't persist for now - FIXME this.deleteExtraPeerDataFile(fileNumber); long uid; try { uid = fs.getLong("uid"); } catch (FSParseException e) { Logger.error(this, "Could not parse offer rejected: "+e+" on "+this+" :\n"+fs, e); return; } FileOffer fo; synchronized(this) { fo = myFileOffersByUID.remove(uid); } fo.onRejected(); } public void handleFproxyBookmarkFeed(SimpleFieldSet fs, int fileNumber) { String name = fs.get("Name"); String description = null; FreenetURI uri = null; boolean hasAnActiveLink = fs.getBoolean("hasAnActivelink", false); long composedTime = fs.getLong("composedTime", -1); long sentTime = fs.getLong("sentTime", -1); long receivedTime = fs.getLong("receivedTime", -1); try { String s = fs.get("Description"); if(s != null) description = Base64.decodeUTF8(s); uri = new FreenetURI(fs.get("URI")); } catch (MalformedURLException e) { Logger.error(this, "Malformed URI in N2NTM Bookmark Feed message"); return; } catch (IllegalBase64Exception e) { Logger.error(this, "Bad Base64 encoding when decoding a N2NTM SimpleFieldSet", e); return; } BookmarkFeedUserAlert userAlert = new BookmarkFeedUserAlert(this, name, description, hasAnActiveLink, fileNumber, uri, composedTime, sentTime, receivedTime); node.clientCore.alerts.register(userAlert); } public void handleFproxyDownloadFeed(SimpleFieldSet fs, int fileNumber) { FreenetURI uri = null; String description = null; long composedTime = fs.getLong("composedTime", -1); long sentTime = fs.getLong("sentTime", -1); long receivedTime = fs.getLong("receivedTime", -1); try { String s = fs.get("Description"); if(s != null) description = Base64.decodeUTF8(s); uri = new FreenetURI(fs.get("URI")); } catch (MalformedURLException e) { Logger.error(this, "Malformed URI in N2NTM File Feed message"); return; } catch (IllegalBase64Exception e) { Logger.error(this, "Bad Base64 encoding when decoding a N2NTM SimpleFieldSet", e); return; } DownloadFeedUserAlert userAlert = new DownloadFeedUserAlert(this, description, fileNumber, uri, composedTime, sentTime, receivedTime); node.clientCore.alerts.register(userAlert); } @Override public String userToString() { return ""+getPeer()+" : "+getName(); } @Override public PeerNodeStatus getStatus(boolean noHeavy) { return new DarknetPeerNodeStatus(this, noHeavy); } @Override public boolean isDarknet() { return true; } @Override public boolean isOpennet() { return false; } @Override public boolean isSeed() { return false; } @Override public void onSuccess(boolean insert, boolean ssk) { // Ignore it } @Override public void onRemove() { // Do nothing // FIXME is there anything we should do? } @Override public boolean isRealConnection() { return true; } @Override public boolean recordStatus() { return true; } @Override public boolean equals(Object o) { if(o == this) return true; // Only equal to seednode of its own type. if(o instanceof DarknetPeerNode) { return super.equals(o); } else return false; } @Override public final boolean shouldDisconnectAndRemoveNow() { return false; } @Override /** Darknet peers clear peerAddedTime on connecting. */ protected void maybeClearPeerAddedTimeOnConnect() { peerAddedTime = 0; // don't store anymore } @Override /** Darknet nodes *do* export the peer added time. However it gets * cleared on connecting: It is only kept for never-connected peers * so we can see that we haven't had a connection in a long time and * offer to get rid of them. */ protected boolean shouldExportPeerAddedTime() { return true; } @Override protected void maybeClearPeerAddedTimeOnRestart(long now) { if((now - peerAddedTime) > DAYS.toMillis(30)) peerAddedTime = 0; if(!neverConnected) peerAddedTime = 0; } // FIXME find a better solution??? @Override public void fatalTimeout() { if(node.isStopping()) return; Logger.error(this, "Disconnecting from darknet node "+this+" because of fatal timeout", new Exception("error")); System.err.println("Your friend node \""+getName()+"\" ("+getPeer()+" version "+getVersion()+") is having severe problems. We have disconnected to try to limit the effect on us. It will reconnect soon."); // FIXME post a useralert // Disconnect. forceDisconnect(); } public synchronized FRIEND_TRUST getTrustLevel() { return trustLevel; } @Override public boolean shallWeRouteAccordingToOurPeersLocation(int htl) { if(!node.shallWeRouteAccordingToOurPeersLocation(htl)) return false; // Globally disabled if(trustLevel == FRIEND_TRUST.LOW) return false; return true; } public void setTrustLevel(FRIEND_TRUST trust) { synchronized(this) { trustLevel = trust; } node.peers.writePeersDarknetUrgent(); } /** FIXME This should be the worse of our visibility for the peer and that which the peer has told us. * I.e. visibility is reciprocal. */ public synchronized FRIEND_VISIBILITY getVisibility() { // ourVisibility can't be null. if(ourVisibility.isStricterThan(theirVisibility)) return ourVisibility; return theirVisibility; } public synchronized FRIEND_VISIBILITY getOurVisibility() { return ourVisibility; } public void setVisibility(FRIEND_VISIBILITY visibility) { synchronized(this) { if(ourVisibility == visibility) return; ourVisibility = visibility; } node.peers.writePeersDarknetUrgent(); try { sendVisibility(); } catch (NotConnectedException e) { Logger.normal(this, "Disconnected while sending visibility update"); } } private void sendVisibility() throws NotConnectedException { sendAsync(DMT.createFNPVisibility(getOurVisibility().code), null, node.nodeStats.initialMessagesCtr); } public void handleVisibility(Message m) { FRIEND_VISIBILITY v = FRIEND_VISIBILITY.getByCode(m.getShort(DMT.FRIEND_VISIBILITY)); if(v == null) { Logger.error(this, "Bogus visibility setting from peer "+this+" : code "+m.getShort(DMT.FRIEND_VISIBILITY)); v = FRIEND_VISIBILITY.NO; } synchronized(this) { if(theirVisibility == v) return; theirVisibility = v; } node.peers.writePeersDarknet(); } public synchronized FRIEND_VISIBILITY getTheirVisibility() { if(theirVisibility == null) return FRIEND_VISIBILITY.NO; return theirVisibility; } @Override boolean dontKeepFullFieldSet() { return false; } private boolean sendingFullNoderef; public void sendFullNoderef() { synchronized(this) { if(sendingFullNoderef) return; // DoS???? sendingFullNoderef = true; } try { SimpleFieldSet myFullNoderef = node.exportDarknetPublicFieldSet(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DeflaterOutputStream dos = new DeflaterOutputStream(baos); try { myFullNoderef.writeTo(dos); dos.close(); } catch (IOException e) { Logger.error(this, "Impossible: Caught error while writing compressed noderef: "+e, e); synchronized(this) { sendingFullNoderef = false; } return; } byte[] data = baos.toByteArray(); long uid = node.fastWeakRandom.nextLong(); RandomAccessBuffer raf = new ByteArrayRandomAccessBuffer(data); PartiallyReceivedBulk prb = new PartiallyReceivedBulk(node.usm, data.length, Node.PACKET_SIZE, raf, true); try { sendAsync(DMT.createFNPMyFullNoderef(uid, data.length), null, node.nodeStats.foafCounter); } catch (NotConnectedException e1) { // Ignore synchronized(this) { sendingFullNoderef = false; } return; } final BulkTransmitter bt; try { bt = new BulkTransmitter(prb, this, uid, false, node.nodeStats.foafCounter, false); } catch (DisconnectedException e) { synchronized(this) { sendingFullNoderef = false; } return; } node.executor.execute(new Runnable() { @Override public void run() { try { bt.send(); } catch (DisconnectedException e) { // :| } finally { synchronized(DarknetPeerNode.this) { sendingFullNoderef = false; } } } }); } catch (RuntimeException e) { synchronized(this) { sendingFullNoderef = false; } throw e; } catch (Error e) { synchronized(this) { sendingFullNoderef = false; } throw e; } } private boolean receivingFullNoderef; public void handleFullNoderef(Message m) { if(this.dontKeepFullFieldSet()) return; long uid = m.getLong(DMT.UID); int length = m.getInt(DMT.NODEREF_LENGTH); if(length > 8 * 1024) { // Way too long! return; } synchronized(this) { if(receivingFullNoderef) return; // DoS???? receivingFullNoderef = true; } try { final byte[] data = new byte[length]; RandomAccessBuffer raf = new ByteArrayRandomAccessBuffer(data); PartiallyReceivedBulk prb = new PartiallyReceivedBulk(node.usm, length, Node.PACKET_SIZE, raf, false); final BulkReceiver br = new BulkReceiver(prb, this, uid, node.nodeStats.foafCounter); node.executor.execute(new Runnable() { @Override public void run() { try { if(br.receive()) { ByteArrayInputStream bais = new ByteArrayInputStream(data); InflaterInputStream dis = new InflaterInputStream(bais); SimpleFieldSet fs; try { fs = new SimpleFieldSet(new BufferedReader(new InputStreamReader(dis, "UTF-8")), false, false); } catch (UnsupportedEncodingException e) { synchronized(DarknetPeerNode.this) { receivingFullNoderef = false; } Logger.error(this, "Impossible: "+e, e); e.printStackTrace(); return; } catch (IOException e) { synchronized(DarknetPeerNode.this) { receivingFullNoderef = false; } Logger.error(this, "Impossible: "+e, e); return; } try { processNewNoderef(fs, false, false, true); } catch (FSParseException e) { Logger.error(this, "Peer "+DarknetPeerNode.this+" sent bogus full noderef: "+e, e); synchronized(DarknetPeerNode.this) { receivingFullNoderef = false; } return; } synchronized(DarknetPeerNode.this) { fullFieldSet = fs; } node.peers.writePeersDarknet(); } else { Logger.error(this, "Failed to receive noderef from "+DarknetPeerNode.this); } } finally { synchronized(DarknetPeerNode.this) { receivingFullNoderef = false; } } } }); } catch (RuntimeException e) { synchronized(this) { receivingFullNoderef = false; } throw e; } catch (Error e) { synchronized(this) { receivingFullNoderef = false; } throw e; } } @Override protected void sendInitialMessages() { super.sendInitialMessages(); try { sendVisibility(); } catch(NotConnectedException e) { Logger.error(this, "Completed handshake with " + getPeer() + " but disconnected: "+e, e); } if(!dontKeepFullFieldSet()) { try { sendAsync(DMT.createFNPGetYourFullNoderef(), null, node.nodeStats.foafCounter); } catch (NotConnectedException e) { // Ignore } } } @Override public boolean isOpennetForNoderef() { return false; } @Override public boolean canAcceptAnnouncements() { return node.passOpennetRefsThroughDarknet(); } @Override protected void writePeers() { node.peers.writePeers(false); } }