gt: 					HTMLNode div = new HTMLNode("div");
context: package freenet.node;
  
  import static java.util.concurrent.TimeUnit.DAYS;
  
  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.Logger.LogLevel;
  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.FileUtil;
  import freenet.support.io.FileRandomAccessBuffer;
  
  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 boolean logMINOR;
  
  	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.
  	 */
  	public DarknetPeerNode(SimpleFieldSet fs, Node node2, NodeCrypto crypto, PeerManager peers, boolean fromLocal, OutgoingPacketMangler mangler, FRIEND_TRUST trust, FRIEND_VISIBILITY visibility2) throws FSParseException, PeerParseException, ReferenceSignatureVerificationException {
  		super(fs, node2, crypto, peers, fromLocal, false, mangler, false);
  
  		logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
  
  		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() {
  