/* * jFCPlib - FpcConnection.java - Copyright © 2008 David Roden * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package net.pterodactylus.fcp; import java.io.Closeable; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import net.pterodactylus.util.logging.Logging; /** * An FCP connection to a Freenet node. * * @author David ‘Bombe’ Roden <bombe@freenetproject.org> */ public class FcpConnection implements Closeable { /** Logger. */ private static final Logger logger = Logging.getLogger(FcpConnection.class.getName()); /** The default port for FCP v2. */ public static final int DEFAULT_PORT = 9481; /** Listener management. */ private final FcpListenerManager fcpListenerManager = new FcpListenerManager(this); /** The address of the node. */ private final InetAddress address; /** The port number of the node’s FCP port. */ private final int port; /** The remote socket. */ private Socket remoteSocket; /** The input stream from the node. */ private InputStream remoteInputStream; /** The output stream to the node. */ private OutputStream remoteOutputStream; /** The connection handler. */ private FcpConnectionHandler connectionHandler; /** Incoming message statistics. */ private static final Map<String, Integer> incomingMessageStatistics = Collections.synchronizedMap(new HashMap<String, Integer>()); /** * Creates a new FCP connection to the freenet node running on localhost, * using the default port. * * @throws UnknownHostException * if the hostname can not be resolved */ public FcpConnection() throws UnknownHostException { this(InetAddress.getLocalHost()); } /** * Creates a new FCP connection to the Freenet node running on the given * host, listening on the default port. * * @param host * The hostname of the Freenet node * @throws UnknownHostException * if <code>host</code> can not be resolved */ public FcpConnection(String host) throws UnknownHostException { this(host, DEFAULT_PORT); } /** * Creates a new FCP connection to the Freenet node running on the given * host, listening on the given port. * * @param host * The hostname of the Freenet node * @param port * The port number of the node’s FCP port * @throws UnknownHostException * if <code>host</code> can not be resolved */ public FcpConnection(String host, int port) throws UnknownHostException { this(InetAddress.getByName(host), port); } /** * Creates a new FCP connection to the Freenet node running at the given * address, listening on the default port. * * @param address * The address of the Freenet node */ public FcpConnection(InetAddress address) { this(address, DEFAULT_PORT); } /** * Creates a new FCP connection to the Freenet node running at the given * address, listening on the given port. * * @param address * The address of the Freenet node * @param port * The port number of the node’s FCP port */ public FcpConnection(InetAddress address, int port) { this.address = address; this.port = port; } // // LISTENER MANAGEMENT // /** * Adds the given listener to the list of listeners. * * @param fcpListener * The listener to add */ public void addFcpListener(FcpListener fcpListener) { fcpListenerManager.addListener(fcpListener); } /** * Removes the given listener from the list of listeners. * * @param fcpListener * The listener to remove */ public void removeFcpListener(FcpListener fcpListener) { fcpListenerManager.removeListener(fcpListener); } // // ACTIONS // /** * Connects to the node. * * @throws IOException * if an I/O error occurs * @throws IllegalStateException * if there is already a connection to the node */ public synchronized void connect() throws IOException, IllegalStateException { if (connectionHandler != null) { throw new IllegalStateException("already connected, disconnect first"); } logger.info("connecting to " + address + ":" + port + "…"); remoteSocket = new Socket(address, port); remoteInputStream = remoteSocket.getInputStream(); remoteOutputStream = remoteSocket.getOutputStream(); new Thread(connectionHandler = new FcpConnectionHandler(this, remoteInputStream)).start(); } /** * Disconnects from the node. If there is no connection to the node, this * method does nothing. * * @deprecated Use {@link #close()} instead */ @Deprecated public synchronized void disconnect() { close(); } /** * Closes the connection. If there is no connection to the node, this method * does nothing. */ public void close() { handleDisconnect(null); } /** * Sends the given FCP message. * * @param fcpMessage * The FCP message to send * @throws IOException * if an I/O error occurs */ public synchronized void sendMessage(FcpMessage fcpMessage) throws IOException { logger.fine("sending message: " + fcpMessage.getName()); fcpMessage.write(remoteOutputStream); } // // PACKAGE-PRIVATE METHODS // /** * Handles the given message, notifying listeners. This message should only * be called by {@link FcpConnectionHandler}. * * @param fcpMessage * The received message */ void handleMessage(FcpMessage fcpMessage) { logger.fine("received message: " + fcpMessage.getName()); String messageName = fcpMessage.getName(); countMessage(messageName); if ("SimpleProgress".equals(messageName)) { fcpListenerManager.fireReceivedSimpleProgress(new SimpleProgress(fcpMessage)); } else if ("ProtocolError".equals(messageName)) { fcpListenerManager.fireReceivedProtocolError(new ProtocolError(fcpMessage)); } else if ("PersistentGet".equals(messageName)) { fcpListenerManager.fireReceivedPersistentGet(new PersistentGet(fcpMessage)); } else if ("PersistentPut".equals(messageName)) { fcpListenerManager.fireReceivedPersistentPut(new PersistentPut(fcpMessage)); } else if ("PersistentPutDir".equals(messageName)) { fcpListenerManager.fireReceivedPersistentPutDir(new PersistentPutDir(fcpMessage)); } else if ("URIGenerated".equals(messageName)) { fcpListenerManager.fireReceivedURIGenerated(new URIGenerated(fcpMessage)); } else if ("EndListPersistentRequests".equals(messageName)) { fcpListenerManager.fireReceivedEndListPersistentRequests(new EndListPersistentRequests(fcpMessage)); } else if ("Peer".equals(messageName)) { fcpListenerManager.fireReceivedPeer(new Peer(fcpMessage)); } else if ("PeerNote".equals(messageName)) { fcpListenerManager.fireReceivedPeerNote(new PeerNote(fcpMessage)); } else if ("StartedCompression".equals(messageName)) { fcpListenerManager.fireReceivedStartedCompression(new StartedCompression(fcpMessage)); } else if ("FinishedCompression".equals(messageName)) { fcpListenerManager.fireReceivedFinishedCompression(new FinishedCompression(fcpMessage)); } else if ("GetFailed".equals(messageName)) { fcpListenerManager.fireReceivedGetFailed(new GetFailed(fcpMessage)); } else if ("PutFetchable".equals(messageName)) { fcpListenerManager.fireReceivedPutFetchable(new PutFetchable(fcpMessage)); } else if ("PutSuccessful".equals(messageName)) { fcpListenerManager.fireReceivedPutSuccessful(new PutSuccessful(fcpMessage)); } else if ("PutFailed".equals(messageName)) { fcpListenerManager.fireReceivedPutFailed(new PutFailed(fcpMessage)); } else if ("DataFound".equals(messageName)) { fcpListenerManager.fireReceivedDataFound(new DataFound(fcpMessage)); } else if ("SubscribedUSKUpdate".equals(messageName)) { fcpListenerManager.fireReceivedSubscribedUSKUpdate(new SubscribedUSKUpdate(fcpMessage)); } else if ("IdentifierCollision".equals(messageName)) { fcpListenerManager.fireReceivedIdentifierCollision(new IdentifierCollision(fcpMessage)); } else if ("AllData".equals(messageName)) { LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength"))); fcpListenerManager.fireReceivedAllData(new AllData(fcpMessage, payloadInputStream)); try { payloadInputStream.consume(); } catch (IOException ioe1) { /* well, ignore. when the connection handler fails, all fails. */ } } else if ("EndListPeerNotes".equals(messageName)) { fcpListenerManager.fireReceivedEndListPeerNotes(new EndListPeerNotes(fcpMessage)); } else if ("EndListPeers".equals(messageName)) { fcpListenerManager.fireReceivedEndListPeers(new EndListPeers(fcpMessage)); } else if ("SSKKeypair".equals(messageName)) { fcpListenerManager.fireReceivedSSKKeypair(new SSKKeypair(fcpMessage)); } else if ("PeerRemoved".equals(messageName)) { fcpListenerManager.fireReceivedPeerRemoved(new PeerRemoved(fcpMessage)); } else if ("PersistentRequestModified".equals(messageName)) { fcpListenerManager.fireReceivedPersistentRequestModified(new PersistentRequestModified(fcpMessage)); } else if ("PersistentRequestRemoved".equals(messageName)) { fcpListenerManager.fireReceivedPersistentRequestRemoved(new PersistentRequestRemoved(fcpMessage)); } else if ("UnknownPeerNoteType".equals(messageName)) { fcpListenerManager.fireReceivedUnknownPeerNoteType(new UnknownPeerNoteType(fcpMessage)); } else if ("UnknownNodeIdentifier".equals(messageName)) { fcpListenerManager.fireReceivedUnknownNodeIdentifier(new UnknownNodeIdentifier(fcpMessage)); } else if ("FCPPluginReply".equals(messageName)) { LimitedInputStream payloadInputStream = getInputStream(FcpUtils.safeParseLong(fcpMessage.getField("DataLength"))); fcpListenerManager.fireReceivedFCPPluginReply(new FCPPluginReply(fcpMessage, payloadInputStream)); try { payloadInputStream.consume(); } catch (IOException ioe1) { /* ignore. */ } } else if ("PluginInfo".equals(messageName)) { fcpListenerManager.fireReceivedPluginInfo(new PluginInfo(fcpMessage)); } else if ("NodeData".equals(messageName)) { fcpListenerManager.fireReceivedNodeData(new NodeData(fcpMessage)); } else if ("TestDDAReply".equals(messageName)) { fcpListenerManager.fireReceivedTestDDAReply(new TestDDAReply(fcpMessage)); } else if ("TestDDAComplete".equals(messageName)) { fcpListenerManager.fireReceivedTestDDAComplete(new TestDDAComplete(fcpMessage)); } else if ("ConfigData".equals(messageName)) { fcpListenerManager.fireReceivedConfigData(new ConfigData(fcpMessage)); } else if ("NodeHello".equals(messageName)) { fcpListenerManager.fireReceivedNodeHello(new NodeHello(fcpMessage)); } else if ("CloseConnectionDuplicateClientName".equals(messageName)) { fcpListenerManager.fireReceivedCloseConnectionDuplicateClientName(new CloseConnectionDuplicateClientName(fcpMessage)); } else if ("SentFeed".equals(messageName)) { fcpListenerManager.fireSentFeed(new SentFeed(fcpMessage)); } else if ("ReceivedBookmarkFeed".equals(messageName)) { fcpListenerManager.fireReceivedBookmarkFeed(new ReceivedBookmarkFeed(fcpMessage)); } else { fcpListenerManager.fireMessageReceived(fcpMessage); } } /** * Handles a disconnect from the node. * * @param throwable * The exception that caused the disconnect, or <code>null</code> * if there was no exception */ synchronized void handleDisconnect(Throwable throwable) { //System.err.println("DISCONNECTED: " + throwable); FcpUtils.close(remoteInputStream); FcpUtils.close(remoteOutputStream); FcpUtils.close(remoteSocket); if (connectionHandler != null) { connectionHandler.stop(); connectionHandler = null; fcpListenerManager.fireConnectionClosed(throwable); } } // // PRIVATE METHODS // /** * Incremets the counter in {@link #incomingMessageStatistics} by * <cod>1</code> for the given message name. * * @param name * The name of the message to count */ private void countMessage(String name) { int oldValue = 0; if (incomingMessageStatistics.containsKey(name)) { oldValue = incomingMessageStatistics.get(name); } incomingMessageStatistics.put(name, oldValue + 1); logger.finest("count for " + name + ": " + (oldValue + 1)); } /** * Returns a limited input stream from the node’s input stream. * * @param dataLength * The length of the stream * @return The limited input stream */ private synchronized LimitedInputStream getInputStream(long dataLength) { if (dataLength <= 0) { return new LimitedInputStream(null, 0); } return new LimitedInputStream(remoteInputStream, dataLength); } /** * A wrapper around an {@link InputStream} that only supplies a limit number * of bytes from the underlying input stream. * * @author David ‘Bombe’ Roden <bombe@freenetproject.org> */ private static class LimitedInputStream extends FilterInputStream { /** The remaining number of bytes that can be read. */ private long remaining; /** * Creates a new LimitedInputStream that supplies at most * <code>length</code> bytes from the given input stream. * * @param inputStream * The input stream * @param length * The number of bytes to read */ public LimitedInputStream(InputStream inputStream, long length) { super(inputStream); remaining = length; } /** * @see java.io.FilterInputStream#available() */ @Override public synchronized int available() throws IOException { if (remaining == 0) { return 0; } return (int) Math.min(super.available(), Math.min(Integer.MAX_VALUE, remaining)); } /** * @see java.io.FilterInputStream#read() */ @Override public synchronized int read() throws IOException { int read = -1; if (remaining > 0) { read = super.read(); remaining--; } return read; } /** * @see java.io.FilterInputStream#read(byte[], int, int) */ @Override public synchronized int read(byte[] b, int off, int len) throws IOException { if (remaining == 0) { return -1; } int toCopy = (int) Math.min(len, Math.min(remaining, Integer.MAX_VALUE)); int read = super.read(b, off, toCopy); remaining -= read; return read; } /** * @see java.io.FilterInputStream#skip(long) */ @Override public synchronized long skip(long n) throws IOException { if ((n < 0) || (remaining == 0)) { return 0; } long skipped = super.skip(Math.min(n, remaining)); remaining -= skipped; return skipped; } /** * {@inheritDoc} This method does nothing, as {@link #mark(int)} and * {@link #reset()} are not supported. * * @see java.io.FilterInputStream#mark(int) */ @Override public synchronized void mark(int readlimit) { /* do nothing. */ } /** * {@inheritDoc} * * @see java.io.FilterInputStream#markSupported() * @return <code>false</code> */ @Override public boolean markSupported() { return false; } /** * {@inheritDoc} This method does nothing, as {@link #mark(int)} and * {@link #reset()} are not supported. * * @see java.io.FilterInputStream#reset() */ @Override public synchronized void reset() throws IOException { /* do nothing. */ } /** * Consumes the input stream, i.e. read all bytes until the limit is * reached. * * @throws IOException * if an I/O error occurs */ public synchronized void consume() throws IOException { while (remaining > 0) { skip(remaining); } } } }