diff --git a/src/main/java/net/consensys/athena/api/cmd/Athena.java b/src/main/java/net/consensys/athena/api/cmd/Athena.java index d6b41e34..56b64280 100644 --- a/src/main/java/net/consensys/athena/api/cmd/Athena.java +++ b/src/main/java/net/consensys/athena/api/cmd/Athena.java @@ -3,11 +3,13 @@ import static java.util.Optional.empty; import net.consensys.athena.api.config.Config; +import net.consensys.athena.api.network.NetworkNodes; import net.consensys.athena.impl.config.TomlConfigBuilder; import net.consensys.athena.impl.http.server.Serializer; import net.consensys.athena.impl.http.server.netty.DefaultNettyServer; import net.consensys.athena.impl.http.server.netty.NettyServer; import net.consensys.athena.impl.http.server.netty.NettySettings; +import net.consensys.athena.impl.network.MemoryNetworkNodes; import java.io.File; import java.io.FileInputStream; @@ -20,12 +22,14 @@ public class Athena { - private static final int DEFAULT_HTTP_PORT = 8080; + private static NetworkNodes networkNodes; public static void main(String[] args) throws Exception { Optional configFileName = args.length > 0 ? Optional.of(args[0]) : Optional.empty(); Config config = loadConfig(configFileName); + networkNodes = new MemoryNetworkNodes(config); + // start http server NettyServer server = startServer(config); joinServer(server); @@ -55,7 +59,7 @@ static NettyServer startServer(Config config) throws InterruptedException { config.socket(), Optional.of((int) config.port()), empty(), - new AthenaRouter(), + new AthenaRouter(networkNodes), new Serializer(new ObjectMapper())); NettyServer server = new DefaultNettyServer(settings); server.start(); diff --git a/src/main/java/net/consensys/athena/api/cmd/AthenaRouter.java b/src/main/java/net/consensys/athena/api/cmd/AthenaRouter.java index 830f3946..f2881685 100644 --- a/src/main/java/net/consensys/athena/api/cmd/AthenaRouter.java +++ b/src/main/java/net/consensys/athena/api/cmd/AthenaRouter.java @@ -1,6 +1,7 @@ package net.consensys.athena.api.cmd; import net.consensys.athena.api.enclave.Enclave; +import net.consensys.athena.api.network.NetworkNodes; import net.consensys.athena.api.storage.KeyValueStore; import net.consensys.athena.api.storage.Storage; import net.consensys.athena.api.storage.StorageIdBuilder; @@ -34,6 +35,11 @@ public class AthenaRouter implements Router { private static final Storage STORAGE = new StorageKeyValueStorageDelegate(KEY_VALUE_STORE, KEY_BUILDER); private static final Serializer SERIALIZER = new Serializer(new ObjectMapper()); + private static NetworkNodes networkNodes; + + public AthenaRouter(NetworkNodes info) { + networkNodes = info; + } @Override public Controller lookup(HttpRequest request) { @@ -61,8 +67,7 @@ public Controller lookup(HttpRequest request) { return new ResendController(ENCLAVE, STORAGE); } if (uri.getPath().startsWith("/partyinfo")) { - //probably need to inject something that stores the party info state. - return new PartyInfoController(); + return new PartyInfoController(networkNodes); } if (uri.getPath().startsWith("/push")) { return new PushController(STORAGE); diff --git a/src/main/java/net/consensys/athena/api/config/Config.java b/src/main/java/net/consensys/athena/api/config/Config.java index 02822656..440db40c 100644 --- a/src/main/java/net/consensys/athena/api/config/Config.java +++ b/src/main/java/net/consensys/athena/api/config/Config.java @@ -1,9 +1,12 @@ package net.consensys.athena.api.config; import java.io.File; +import java.net.URL; import java.util.Optional; -/** Configuration for Athena Refer to the "sample.conf" file for documentation on config elements */ +/** + * Configuration for Athena. Refer to the "sample.conf" file for documentation on config elements + */ public interface Config { /** @@ -12,7 +15,7 @@ public interface Config { * * @return URL for this node */ - String url(); + URL url(); /** * Port to listen on for the public API. @@ -47,7 +50,7 @@ public interface Config { * * @return Array of other node URLs to connect to on startup */ - File[] otherNodes(); + URL[] otherNodes(); /** * The set of public keys this node will host. @@ -182,7 +185,7 @@ public interface Config { * after populating the tlsKnownClients list to restrict access. *
  • ca: Only nodes with a valid certificate and chain of trust to one of the * system root certificates will be allowed to connect. The folder containing trusted root - * certificates can be overridden with the SYSTEM_CERTIFICATE_PATH environment variable. + * certificates can be overriden with the SYSTEM_CERTIFICATE_PATH environment variable. *
  • ca-or-tofu: A combination of ca and tofu: If a certificate is valid, it * is always allowed and added to the tlsKnownClients list. If it is self-signed, it * will be allowed only if it's the first certificate this node has seen for that host. @@ -250,7 +253,7 @@ public interface Config { * server for any given host. (Similar to how OpenSSH works.) *
  • ca: The node will only connect to servers with a valid certificate and * chain of trust to one of the system root certificates. The folder containing trusted root - * certificates can be overridden with the SYSTEM_CERTIFICATE_PATH environment variable. + * certificates can be overriden with the SYSTEM_CERTIFICATE_PATH environment variable. *
  • ca-or-tofu: A combination of ca and tofu: If a certificate is valid, it * is always allowed and added to the tlsKnownServers list. If it is self-signed, it * will be allowed only if it's the first certificate this node has seen for that host. diff --git a/src/main/java/net/consensys/athena/api/network/NetworkNodes.java b/src/main/java/net/consensys/athena/api/network/NetworkNodes.java new file mode 100644 index 00000000..2fde11b3 --- /dev/null +++ b/src/main/java/net/consensys/athena/api/network/NetworkNodes.java @@ -0,0 +1,31 @@ +package net.consensys.athena.api.network; + +import java.net.URL; +import java.security.PublicKey; +import java.util.HashMap; +import java.util.HashSet; + +/** Details of other nodes on the network */ +public interface NetworkNodes { + + /** + * URL of this node + * + * @return URL of node + */ + URL url(); + + /** + * List of URLs of other nodes on the network + * + * @return + */ + HashSet nodeURLs(); + + /** + * Map from URL to public key for all discovered nodes. + * + * @return + */ + HashMap nodePKs(); +} diff --git a/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java b/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java index f0dff0a6..670a03c6 100644 --- a/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java +++ b/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java @@ -3,15 +3,16 @@ import net.consensys.athena.api.config.Config; import java.io.File; +import java.net.URL; import java.util.Optional; public class MemoryConfig implements Config { - private String url; + private URL url; private long port; private Optional workDir = Optional.empty(); private Optional socket = Optional.empty(); - private File[] otherNodes; + private URL[] otherNodes; private File[] publicKeys; private File[] privateKeys; private File[] alwaysSendTo; @@ -33,7 +34,7 @@ public class MemoryConfig implements Config { private Optional showVersion = Optional.empty(); private long verbosity; - public void setUrl(String url) { + public void setUrl(URL url) { this.url = url; } @@ -49,7 +50,7 @@ public void setSocket(File socket) { this.socket = Optional.ofNullable(socket); } - public void setOtherNodes(File[] otherNodes) { + public void setOtherNodes(URL[] otherNodes) { this.otherNodes = otherNodes; } @@ -134,7 +135,7 @@ public void setVerbosity(long verbosity) { } @Override - public String url() { + public URL url() { return url; } @@ -154,7 +155,7 @@ public Optional socket() { } @Override - public File[] otherNodes() { + public URL[] otherNodes() { return otherNodes; } diff --git a/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java b/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java index 58cd9275..e9117382 100644 --- a/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java +++ b/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java @@ -5,6 +5,8 @@ import java.io.File; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; import java.util.Arrays; import java.util.List; @@ -19,7 +21,12 @@ public Config build(InputStream config) throws ConfigException { Toml toml = new Toml().read(config); if (toml.getString("url") != null) { - memoryConfig.setUrl(toml.getString("url")); + try { + memoryConfig.setUrl(new URL(toml.getString("url"))); + } catch (MalformedURLException e) { + errorMsg.append("Error: key 'url' in config is malformed.\n\t"); + errorMsg.append(e.getMessage()).append("\n"); + } } else { errorMsg.append("Error: value for key 'url' in config must be specified\n"); } @@ -38,7 +45,12 @@ public Config build(InputStream config) throws ConfigException { memoryConfig.setSocket(new File(toml.getString("socket"))); } - memoryConfig.setOtherNodes(convertListToFileArray(toml.getList("othernodes"))); + try { + memoryConfig.setOtherNodes(convertListToURLArray(toml.getList("othernodes"))); + } catch (ConfigException e) { + errorMsg.append("Error: key 'othernodes' in config containes malformed URLS.\n"); + errorMsg.append(e.getMessage()); + } memoryConfig.setPublicKeys(convertListToFileArray(toml.getList("publickeys"))); memoryConfig.setPrivateKeys(convertListToFileArray(toml.getList("privatekeys"))); @@ -113,6 +125,34 @@ private File[] convertListToFileArray(List paths) { return paths == null ? new File[0] : paths.stream().map(File::new).toArray(File[]::new); } + private URL[] convertListToURLArray(List urls) { + URL[] urlArray; + StringBuilder errorMsg = new StringBuilder(); + + if (urls == null) { + urlArray = new URL[0]; + } else { + urlArray = new URL[urls.size()]; + for (int i = 0; i < urls.size(); i++) { + try { + urlArray[i] = new URL(urls.get(i)); + } catch (MalformedURLException e) { + errorMsg + .append("\tURL [") + .append(urls.get(i)) + .append("] ") + .append(e.getMessage()) + .append("\n"); + } + } + } + if (errorMsg.length() != 0) { + throw new ConfigException(errorMsg.toString()); + } + + return urlArray; + } + private String[] convertListToStringArray(List paths) { return paths == null ? new String[0] : paths.toArray(new String[paths.size()]); } diff --git a/src/main/java/net/consensys/athena/impl/http/controllers/PartyInfoController.java b/src/main/java/net/consensys/athena/impl/http/controllers/PartyInfoController.java index d9654fdf..c7b80a26 100644 --- a/src/main/java/net/consensys/athena/impl/http/controllers/PartyInfoController.java +++ b/src/main/java/net/consensys/athena/impl/http/controllers/PartyInfoController.java @@ -1,5 +1,6 @@ package net.consensys.athena.impl.http.controllers; +import net.consensys.athena.api.network.NetworkNodes; import net.consensys.athena.impl.http.server.ContentType; import net.consensys.athena.impl.http.server.Controller; import net.consensys.athena.impl.http.server.Result; @@ -12,8 +13,14 @@ */ public class PartyInfoController implements Controller { + private static NetworkNodes networkNodes; + + public PartyInfoController(NetworkNodes info) { + networkNodes = info; + } + @Override public Result handle(FullHttpRequest request) { - return Result.notImplemented(ContentType.HASKELL_ENCODED); + return Result.ok(ContentType.HASKELL_ENCODED, networkNodes); } } diff --git a/src/main/java/net/consensys/athena/impl/network/MemoryNetworkNodes.java b/src/main/java/net/consensys/athena/impl/network/MemoryNetworkNodes.java new file mode 100644 index 00000000..ed6bd8d0 --- /dev/null +++ b/src/main/java/net/consensys/athena/impl/network/MemoryNetworkNodes.java @@ -0,0 +1,67 @@ +package net.consensys.athena.impl.network; + +import net.consensys.athena.api.config.Config; +import net.consensys.athena.api.network.NetworkNodes; + +import java.net.URL; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; + +public class MemoryNetworkNodes implements NetworkNodes { + + private URL url; + private HashSet nodeURLs; + private HashMap nodePKs; + + public MemoryNetworkNodes() { + nodeURLs = new HashSet<>(); + nodePKs = new HashMap<>(); + } + + public MemoryNetworkNodes(Config config) { + url = config.url(); + if (config.otherNodes().length > 0) { + nodeURLs = new HashSet<>(Arrays.asList(config.otherNodes())); + } else { + nodeURLs = new HashSet<>(); + } + + nodePKs = new HashMap<>(); + } + + /** + * Add the URL of a node to the nodeURLs list. + * + * @param node URL of new node + */ + public void addNodeURL(URL node) { + this.nodeURLs.add(node); + } + + /** + * Add a node's URL and PublcKey to the nodeURLs and nodePKs lists + * + * @param node URL of new node + * @param nodePk PublicKey of new node + */ + public void addNode(URL node, PublicKey nodePk) { + this.nodeURLs.add(node); + this.nodePKs.put(node, nodePk); + } + + @Override + public URL url() { + return url; + } + + @Override + public HashSet nodeURLs() { + return nodeURLs; + } + + public HashMap nodePKs() { + return nodePKs; + } +} diff --git a/src/test/java/net/consensys/athena/api/cmd/AthenaRouterTest.java b/src/test/java/net/consensys/athena/api/cmd/AthenaRouterTest.java index 98fe0503..ab9c7c41 100644 --- a/src/test/java/net/consensys/athena/api/cmd/AthenaRouterTest.java +++ b/src/test/java/net/consensys/athena/api/cmd/AthenaRouterTest.java @@ -10,6 +10,7 @@ import net.consensys.athena.impl.http.controllers.SendController; import net.consensys.athena.impl.http.controllers.UpcheckController; import net.consensys.athena.impl.http.server.Controller; +import net.consensys.athena.impl.network.MemoryNetworkNodes; import java.util.Arrays; import java.util.Collection; @@ -28,7 +29,7 @@ public class AthenaRouterTest { private final String testName; private final String path; private final Class controllerClass; - AthenaRouter router = new AthenaRouter(); + AthenaRouter router = new AthenaRouter(new MemoryNetworkNodes()); @Parameterized.Parameters public static Collection routes() { @@ -42,7 +43,7 @@ public static Collection routes() { {"Receive", "/receive", ReceiveController.class}, {"Delete", "/delete", DeleteController.class}, {"Resend", "/resend", ResendController.class}, - {"PartyInfo", "/partyinfo", PartyInfoController.class}, + {"NetworkNodes", "/partyinfo", PartyInfoController.class}, {"Push", "/push", PushController.class}, }); } diff --git a/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java b/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java index e61d46ab..6d59b1da 100644 --- a/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java +++ b/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java @@ -7,6 +7,7 @@ import java.io.File; import java.io.InputStream; +import java.net.URL; import org.junit.Test; @@ -24,7 +25,8 @@ public void testFullFileRead() throws Exception { Config testConf = configBuilder.build(configAsStream); - assertEquals("http://127.0.0.1:9001/", testConf.url()); + URL expectedURL = new URL("http://127.0.0.1:9001/"); + assertEquals(expectedURL, testConf.url()); assertEquals(9001, testConf.port()); assertEquals("memory", testConf.storage()); assertEquals("off", testConf.tls()); @@ -46,10 +48,6 @@ public void testFullFileRead() throws Exception { assertEquals(expectedFile, testConf.passwords().get()); // File Arrays - expectedFilesArray = new File[1]; - expectedFilesArray[0] = new File("http://127.0.0.1:9000/"); - assertArrayEquals(expectedFilesArray, testConf.otherNodes()); - expectedFilesArray = new File[1]; expectedFilesArray[0] = new File("foo.pub"); assertArrayEquals(expectedFilesArray, testConf.publicKeys()); @@ -68,6 +66,11 @@ public void testFullFileRead() throws Exception { expectedFilesArray = new File[0]; assertArrayEquals(expectedFilesArray, testConf.tlsClientChain()); + // URL Array + URL[] expectedURLArray = new URL[1]; + expectedURLArray[0] = new URL("http://127.0.0.1:9000/"); + assertArrayEquals(expectedURLArray, testConf.otherNodes()); + // String Array String expectedStringArray[] = {"10.0.0.1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}; assertArrayEquals(expectedStringArray, testConf.ipWhitelist()); @@ -154,6 +157,10 @@ public void testInvalidConfigsThrowException() throws Exception { } catch (ConfigException e) { String message = "Invalid Configuration Options\n" + + "Error: key 'url' in config is malformed.\n\tunknown protocol: htt\n" + + "Error: key 'othernodes' in config containes malformed URLS.\n" + + "\tURL [htt://127.0.0.1:9000/] unknown protocol: htt\n" + + "\tURL [10.1.1.1] no protocol: 10.1.1.1\n" + "Error: the number of keys specified for keys 'publickeys' and 'privatekeys' must be the same\n" + "Error: value for key 'storage' type must start with: ['bdp:', 'dir:', 'leveldb:', 'sqllite:'] or be 'memory'\n" + "Error: value for key 'tls' status must be 'strict' or 'off'\n" diff --git a/src/test/java/net/consensys/athena/impl/http/controllers/PartyInfoControllerTest.java b/src/test/java/net/consensys/athena/impl/http/controllers/PartyInfoControllerTest.java new file mode 100644 index 00000000..9dbbf130 --- /dev/null +++ b/src/test/java/net/consensys/athena/impl/http/controllers/PartyInfoControllerTest.java @@ -0,0 +1,39 @@ +package net.consensys.athena.impl.http.controllers; + +import static org.junit.Assert.*; + +import net.consensys.athena.api.network.NetworkNodes; +import net.consensys.athena.impl.config.MemoryConfig; +import net.consensys.athena.impl.http.helpers.HttpTester; +import net.consensys.athena.impl.http.server.Controller; +import net.consensys.athena.impl.http.server.Result; +import net.consensys.athena.impl.network.MemoryNetworkNodes; + +import java.net.URL; +import java.util.Optional; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.junit.Test; + +public class PartyInfoControllerTest { + @Test + public void testSuccessfullProcessingOfRequest() throws Exception { + + MemoryConfig config = new MemoryConfig(); + + config.setUrl(new URL("http://127.0.0.1:9001/")); + URL[] otherNodes = new URL[1]; + otherNodes[0] = new URL("http://127.0.0.1:9000/"); + config.setOtherNodes(otherNodes); + + NetworkNodes networkNodes = new MemoryNetworkNodes(config); + Controller controller = new PartyInfoController(networkNodes); + + Result result = + new HttpTester(controller).uri("/partyinfo").method(HttpMethod.POST).sendRequest(); + + assertEquals(result.getStatus().code(), HttpResponseStatus.OK.code()); + assertEquals(Optional.of(networkNodes), result.getPayload()); + } +} diff --git a/src/test/java/net/consensys/athena/impl/http/server/netty/DefaultNettyServerTest.java b/src/test/java/net/consensys/athena/impl/http/server/netty/DefaultNettyServerTest.java index b9a648c5..fd6ab7a8 100644 --- a/src/test/java/net/consensys/athena/impl/http/server/netty/DefaultNettyServerTest.java +++ b/src/test/java/net/consensys/athena/impl/http/server/netty/DefaultNettyServerTest.java @@ -6,6 +6,7 @@ import net.consensys.athena.api.cmd.AthenaRouter; import net.consensys.athena.impl.http.server.Serializer; +import net.consensys.athena.impl.network.MemoryNetworkNodes; import java.io.BufferedReader; import java.io.DataOutputStream; @@ -26,7 +27,11 @@ public void testStartWillStartTheServerAndListenOnHttpPortFromSettings() int port = getPortWithNothingRunningOnIt(); NettySettings settings = new NettySettings( - empty(), of(port), empty(), new AthenaRouter(), new Serializer(new ObjectMapper())); + empty(), + of(port), + empty(), + new AthenaRouter(new MemoryNetworkNodes()), + new Serializer(new ObjectMapper())); NettyServer server = new DefaultNettyServer(settings); server.start(); URL url = new URL("http://localhost:" + port + "/upcheck"); diff --git a/src/test/resources/invalidConfigTest.toml b/src/test/resources/invalidConfigTest.toml index 2ae68636..c8764989 100644 --- a/src/test/resources/invalidConfigTest.toml +++ b/src/test/resources/invalidConfigTest.toml @@ -1,6 +1,7 @@ # Test config with invalid config settings to test validation exception -url = "http://127.0.0.1:9001/" +url = "htt://127.0.0.1:9001/" port = 9001 +othernodes = ["htt://127.0.0.1:9000/", "10.1.1.1"] verbosity = 4 tls = "notValid" tlsservertrust = "invalid-trust-mode"