Generate SSK key pair from first connected node.
[jSite2.git] / src / net / pterodactylus / jsite / core / NodeManager.java
1 /*
2  * jSite2 - FcpCollector.java -
3  * Copyright © 2008 David Roden
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 package net.pterodactylus.jsite.core;
21
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.OutputStream;
30 import java.net.UnknownHostException;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Properties;
39 import java.util.Set;
40 import java.util.logging.Level;
41 import java.util.logging.Logger;
42
43 import net.pterodactylus.fcp.SSKKeypair;
44 import net.pterodactylus.fcp.highlevel.FcpClient;
45 import net.pterodactylus.fcp.highlevel.FcpException;
46 import net.pterodactylus.jsite.util.IdGenerator;
47 import net.pterodactylus.util.io.Closer;
48 import net.pterodactylus.util.logging.Logging;
49 import net.pterodactylus.util.number.Hex;
50
51 /**
52  * TODO
53  *
54  * @author David ‘Bombe’ Roden <bombe@freenetproject.org>
55  */
56 public class NodeManager implements Iterable<Node>, PropertyChangeListener {
57
58         /** Logger. */
59         private static final Logger logger = Logging.getLogger(NodeManager.class.getName());
60
61         /** The FCP client name. */
62         private final String clientName;
63
64         /** The directory for the configuration. */
65         private final String directory;
66
67         /** Object used for synchronization. */
68         private final Object syncObject = new Object();
69
70         /** Node listener support. */
71         private final NodeListenerSupport nodeListenerManager = new NodeListenerSupport();
72
73         /** All nodes. */
74         private final List<Node> nodes = Collections.synchronizedList(new ArrayList<Node>());
75
76         /** Map from node ID to node. */
77         private final Map<String, Node> idNodes = Collections.synchronizedMap(new HashMap<String, Node>());
78
79         /** Map from node to client. */
80         private final Map<Node, FcpClient> nodeClients = Collections.synchronizedMap(new HashMap<Node, FcpClient>());
81
82         /** Collection of currently connected nodes. */
83         private final Set<Node> connectedNodes = Collections.synchronizedSet(new HashSet<Node>());
84
85         /**
86          * Creates a new FCP collector.
87          *
88          * @param clientName
89          *            The name of the FCP client
90          * @param directory
91          *            The directory in which to store the nodes
92          */
93         public NodeManager(String clientName, String directory) {
94                 this.clientName = clientName;
95                 this.directory = directory;
96         }
97
98         //
99         // EVENT MANAGEMENT
100         //
101
102         /**
103          * Adds the given listener to the list of listeners.
104          *
105          * @param nodeListener
106          *            The listener to add
107          */
108         public void addNodeListener(NodeListener nodeListener) {
109                 nodeListenerManager.addListener(nodeListener);
110         }
111
112         /**
113          * Removes the given listener from the list of listeners.
114          *
115          * @param nodeListener
116          *            The listener to remove
117          */
118         public void removeNodeListener(NodeListener nodeListener) {
119                 nodeListenerManager.removeListener(nodeListener);
120         }
121
122         //
123         // ACCESSORS
124         //
125
126         /**
127          * Returns the directory in which the nodes are stored.
128          *
129          * @return The directory the nodes are stored in
130          */
131         public String getDirectory() {
132                 return directory;
133         }
134
135         /**
136          * Checks whether the given node is already connected.
137          *
138          * @param node
139          *            The node to check
140          * @return <code>true</code> if the node is already connected,
141          *         <code>false</code> otherwise
142          */
143         public boolean hasNode(Node node) {
144                 return nodes.contains(node);
145         }
146
147         /**
148          * Returns whether the given node is currently connected.
149          *
150          * @param node
151          *            The node to check
152          * @return <code>true</code> if the node is currently connected,
153          *         <code>false</code> otherwise
154          */
155         public boolean isNodeConnected(Node node) {
156                 return connectedNodes.contains(node);
157         }
158
159         /**
160          * {@inheritDoc}
161          */
162         public Iterator<Node> iterator() {
163                 return nodes.iterator();
164         }
165
166         //
167         // ACTIONS
168         //
169
170         /**
171          * Loads nodes.
172          *
173          * @throws IOException
174          *             if an I/O error occurs loading the nodes
175          */
176         public void load() throws IOException {
177                 logger.log(Level.FINEST, "load()");
178                 File directoryFile = new File(directory);
179                 File nodeFile = new File(directoryFile, "nodes.properties");
180                 if (!nodeFile.exists() || !nodeFile.isFile() || !nodeFile.canRead()) {
181                         return;
182                 }
183                 Properties nodeProperties = new Properties();
184                 InputStream nodeInputStream = null;
185                 try {
186                         nodeInputStream = new FileInputStream(nodeFile);
187                         nodeProperties.load(nodeInputStream);
188                 } finally {
189                         Closer.close(nodeInputStream);
190                 }
191                 int nodeIndex = -1;
192                 List<Node> loadedNodes = new ArrayList<Node>();
193                 while (nodeProperties.containsKey("nodes." + ++nodeIndex + ".name")) {
194                         String nodePrefix = "nodes." + nodeIndex;
195                         String nodeId = nodeProperties.getProperty(nodePrefix + ".id");
196                         if (nodeId == null) {
197                                 nodeId = Hex.toHex(IdGenerator.generateId());
198                         }
199                         String nodeName = nodeProperties.getProperty(nodePrefix + ".name");
200                         if (!Verifier.verifyNodeName(nodeName)) {
201                                 logger.log(Level.WARNING, "invalid node name “" + nodeName + "”, skipping…");
202                                 continue;
203                         }
204                         String nodeHostname = nodeProperties.getProperty(nodePrefix + ".hostname");
205                         if (!Verifier.verifyHostname(nodeHostname)) {
206                                 logger.log(Level.WARNING, "invalid host name “" + nodeHostname + "”");
207                                 /* not fatal, might be valid later on. */
208                         }
209                         String nodePortString = nodeProperties.getProperty(nodePrefix + ".port");
210                         if (!Verifier.verifyPort(nodePortString)) {
211                                 logger.log(Level.WARNING, "invalid port number “" + nodePortString + "”, skipping…");
212                                 continue;
213                         }
214                         int nodePort = -1;
215                         try {
216                                 nodePort = Integer.valueOf(nodePortString);
217                         } catch (NumberFormatException nfe1) {
218                                 /* shouldn't happen, port number was checked before. */
219                                 logger.log(Level.SEVERE, "invalid port number “" + nodePortString + "”, check failed! skipping…");
220                                 continue;
221                         }
222                         Node newNode = new Node();
223                         newNode.setId(nodeId);
224                         newNode.setName(nodeName);
225                         newNode.setHostname(nodeHostname);
226                         newNode.setPort(nodePort);
227                         loadedNodes.add(newNode);
228                 }
229                 logger.log(Level.FINE, "loaded " + loadedNodes.size() + " nodes from config");
230                 synchronized (syncObject) {
231                         nodes.clear();
232                         for (Node node : loadedNodes) {
233                                 addNode(node);
234                         }
235                 }
236         }
237
238         /**
239          * Saves all configured nodes.
240          *
241          * @throws IOException
242          *             if an I/O error occurs saving the nodes
243          */
244         public void save() throws IOException {
245                 logger.log(Level.FINEST, "save()");
246                 File directoryFile = new File(directory);
247                 if (!directoryFile.exists()) {
248                         if (!directoryFile.mkdirs()) {
249                                 throw new IOException("could not create directory: " + directory);
250                         }
251                 }
252                 Properties nodeProperties = new Properties();
253                 int nodeIndex = -1;
254                 for (Node node : nodes) {
255                         String nodePrefix = "nodes." + ++nodeIndex;
256                         nodeProperties.setProperty(nodePrefix + ".id", node.getId());
257                         nodeProperties.setProperty(nodePrefix + ".name", node.getName());
258                         nodeProperties.setProperty(nodePrefix + ".hostname", node.getHostname());
259                         nodeProperties.setProperty(nodePrefix + ".port", String.valueOf(node.getPort()));
260                 }
261                 File projectFile = new File(directoryFile, "nodes.properties");
262                 OutputStream nodeOutputStream = null;
263                 try {
264                         nodeOutputStream = new FileOutputStream(projectFile);
265                         nodeProperties.store(nodeOutputStream, "jSite nodes");
266                 } finally {
267                         Closer.close(nodeOutputStream);
268                 }
269         }
270
271         /**
272          * Adds the given node to this manager.
273          *
274          * @see #connect(Node)
275          * @param node
276          *            The node to connect to
277          * @return <code>true</code> if the node was added, <code>false</code> if
278          *         the node was not added because it was already known
279          */
280         public boolean addNode(Node node) {
281                 logger.log(Level.FINEST, "addNode(node=" + node + ")");
282                 if (nodes.contains(node)) {
283                         logger.log(Level.WARNING, "was told to add already known node: " + node);
284                         return false;
285                 }
286                 node.addPropertyChangeListener(this);
287                 nodes.add(node);
288                 idNodes.put(node.getId(), node);
289                 nodeListenerManager.fireNodeAdded(node);
290                 return true;
291         }
292
293         /**
294          * Removes the given node from the node manager, disconnecting it if it is
295          * currently connected.
296          *
297          * @param node
298          *            The node to remove
299          */
300         public void removeNode(Node node) {
301                 logger.log(Level.FINEST, "removeNode(node=" + node + ")");
302                 synchronized (syncObject) {
303                         if (!nodes.contains(node)) {
304                                 return;
305                         }
306                         nodes.remove(node);
307                         idNodes.remove(node.getId());
308                         node.removePropertyChangeListener(this);
309                         nodeListenerManager.fireNodeRemoved(node);
310                 }
311         }
312
313         /**
314          * Tries to establish a connection with the given node.
315          *
316          * @param node
317          *            The node to connect to
318          */
319         public void connect(Node node) {
320                 logger.log(Level.FINEST, "connect(node=" + node + ")");
321                 if (!nodes.contains(node)) {
322                         logger.log(Level.WARNING, "Was told to connect to node (" + node + ") I don’t know about!");
323                         return;
324                 }
325                 try {
326                         FcpClient fcpClient = new FcpClient(clientName, node.getHostname(), node.getPort());
327                         fcpClient.connect();
328                         nodeClients.put(node, fcpClient);
329                         nodeListenerManager.fireNodeConnected(node);
330                 } catch (UnknownHostException uhe1) {
331                         nodeListenerManager.fireNodeConnectionFailed(node, uhe1);
332                 } catch (IOException ioe1) {
333                         nodeListenerManager.fireNodeConnectionFailed(node, ioe1);
334                 } catch (FcpException fe1) {
335                         nodeListenerManager.fireNodeConnectionFailed(node, fe1);
336                 }
337         }
338
339         /**
340          * Disconnects the given node without removing it.
341          *
342          * @param node
343          *            The node to disconnect
344          */
345         public void disconnect(Node node) {
346                 logger.log(Level.FINEST, "disconnect(node=" + node + ")");
347                 if (!nodes.contains(node)) {
348                         logger.log(Level.WARNING, "Was told to disconnect from a node (" + node + ") I don’t know about!");
349                         return;
350                 }
351                 FcpClient fcpClient = nodeClients.remove(node);
352                 if (fcpClient == null) {
353                         logger.log(Level.WARNING, "No FCP client for node (" + node + ")!");
354                         return;
355                 }
356                 fcpClient.disconnect();
357                 nodeListenerManager.fireNodeDisconnected(node, null);
358         }
359
360         /**
361          * Returns a list of all nodes.
362          *
363          * @return A list of all nodes
364          */
365         public List<Node> getNodes() {
366                 return Collections.unmodifiableList(nodes);
367         }
368
369         /**
370          * Returns the node identified by the given ID.
371          *
372          * @param id
373          *            The ID of the node
374          * @return The node with the given ID, or <code>null</code> if no such node
375          *         was found
376          */
377         Node getNode(String id) {
378                 return idNodes.get(id);
379         }
380
381         /**
382          * Generates a new SSK key pair.
383          *
384          * @return An array with the private key at index <code>0</code> and the
385          *         public key at index <code>1</code>
386          * @throws IOException
387          *             if an I/O error occurs communicating with the node
388          * @throws JSiteException
389          *             if there is no connected node
390          */
391         public String[] generateKeyPair() throws IOException, JSiteException {
392                 logger.log(Level.FINEST, "generateKeyPair()");
393                 if (nodes.isEmpty()) {
394                         throw new NoNodeException("no node configured");
395                 }
396                 for (FcpClient fcpClient : nodeClients.values()) {
397                         try {
398                                 SSKKeypair sskKeypair = fcpClient.generateKeyPair();
399                                 return new String[] { sskKeypair.getInsertURI(), sskKeypair.getRequestURI() };
400                         } catch (FcpException fcpe1) {
401                                 /* ignore, we’ll throw later on if needs be. */
402                         }
403                 }
404                 throw new JSiteException("Could not get SSK key pair from any node.");
405         }
406
407         //
408         // PRIVATE METHODS
409         //
410
411         //
412         // INTERFACE PropertyChangeListener
413         //
414
415         /**
416          * {@inheritDoc}
417          */
418         public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
419                 Object eventSource = propertyChangeEvent.getSource();
420                 if (eventSource instanceof Node) {
421                         String propertyName = propertyChangeEvent.getPropertyName();
422                         if ("hostname".equals(propertyName) || "port".equals(propertyName)) {
423                                 /* TODO - reconnect. */
424                         }
425                 }
426         }
427
428 }