diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java index 82ad466527..5d7f5e3276 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java @@ -89,6 +89,7 @@ public class PantheonNode implements Node, NodeConfiguration, RunnableNode, Auto private JsonRequestFactories jsonRequestFactories; private HttpRequestFactory httpRequestFactory; private Optional ethNetworkConfig = Optional.empty(); + private boolean useWsForJsonRpc = false; public PantheonNode( final String name, @@ -158,6 +159,18 @@ private Optional wsRpcBaseUrl() { } } + private Optional wsRpcBaseHttpUrl() { + if (isWebSocketsRpcEnabled()) { + return Optional.of( + "http://" + + webSocketConfiguration.getHost() + + ":" + + portsProperties.getProperty("ws-rpc")); + } else { + return Optional.empty(); + } + } + @Override public Optional jsonRpcWebSocketPort() { if (isWebSocketsRpcEnabled()) { @@ -174,10 +187,19 @@ public String hostName() { private JsonRequestFactories jsonRequestFactories() { if (jsonRequestFactories == null) { + final Optional baseUrl; + final String port; + if (useWsForJsonRpc) { + baseUrl = wsRpcBaseUrl(); + port = "8546"; + } else { + baseUrl = jsonRpcBaseUrl(); + port = "8545"; + } final Web3jService web3jService = - jsonRpcBaseUrl() + baseUrl .map(url -> new HttpService(url)) - .orElse(new HttpService("http://" + LOCALHOST + ":8545")); + .orElse(new HttpService("http://" + LOCALHOST + ":" + port)); jsonRequestFactories = new JsonRequestFactories( @@ -193,8 +215,17 @@ private JsonRequestFactories jsonRequestFactories() { private HttpRequestFactory httpRequestFactory() { if (httpRequestFactory == null) { + final Optional baseUrl; + final String port; + if (useWsForJsonRpc) { + baseUrl = wsRpcBaseHttpUrl(); + port = "8546"; + } else { + baseUrl = jsonRpcBaseUrl(); + port = "8545"; + } httpRequestFactory = - new HttpRequestFactory(jsonRpcBaseUrl().orElse("http://" + LOCALHOST + ":8545")); + new HttpRequestFactory(baseUrl.orElse("http://" + LOCALHOST + ":" + port)); } return httpRequestFactory; } @@ -216,6 +247,12 @@ public void useWebSocketsForJsonRpc() { if (jsonRequestFactories != null) { jsonRequestFactories.shutdown(); } + + if (httpRequestFactory != null) { + httpRequestFactory = null; + } + + useWsForJsonRpc = true; } private void checkIfWebSocketEndpointIsAvailable(final String url) { diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java index 26642f56af..4c21457f50 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java @@ -75,10 +75,10 @@ public void startNode(final PantheonNode node) { params.add(String.join(",", node.bootnodes().toString())); if (node.jsonRpcEnabled()) { - params.add("--rpc-enabled"); + params.add("--rpc-http-enabled"); params.add("--rpc-listen"); params.add(node.jsonRpcListenAddress().get()); - params.add("--rpc-api"); + params.add("--rpc-http-api"); params.add(apiList(node.jsonRpcConfiguration().getRpcApis())); if (node.jsonRpcConfiguration().isAuthenticationEnabled()) { params.add("--rpc-http-authentication-enabled"); @@ -90,11 +90,18 @@ public void startNode(final PantheonNode node) { } if (node.wsRpcEnabled()) { - params.add("--ws-enabled"); + params.add("--rpc-ws-enabled"); params.add("--ws-listen"); params.add(node.wsRpcListenAddress().get()); - params.add("--ws-api"); + params.add("--rpc-ws-api"); params.add(apiList(node.webSocketConfiguration().getRpcApis())); + if (node.webSocketConfiguration().isAuthenticationEnabled()) { + params.add("--rpc-ws-authentication-enabled"); + } + if (node.webSocketConfiguration().getAuthenticationCredentialsFile() != null) { + params.add("--rpc-ws-authentication-credentials-file"); + params.add(node.webSocketConfiguration().getAuthenticationCredentialsFile()); + } } if (node.ethNetworkConfig().isPresent()) { diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java index 72b1f2d7bf..0cc2d55854 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonFactoryConfigurationBuilder.java @@ -104,6 +104,19 @@ public PantheonFactoryConfigurationBuilder webSocketEnabled() { return this; } + public PantheonFactoryConfigurationBuilder webSocketAuthenticationEnabled() + throws URISyntaxException { + final String authTomlPath = + Paths.get(ClassLoader.getSystemResource("authentication/auth.toml").toURI()) + .toAbsolutePath() + .toString(); + + this.webSocketConfiguration.setAuthenticationEnabled(true); + this.webSocketConfiguration.setAuthenticationCredentialsFile(authTomlPath); + + return this; + } + public PantheonFactoryConfigurationBuilder setPermissioningConfiguration( final PermissioningConfiguration permissioningConfiguration) { this.permissioningConfiguration = Optional.of(permissioningConfiguration); diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java index 00b2a7168c..adb18834e4 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/factory/PantheonNodeFactory.java @@ -126,6 +126,16 @@ public PantheonNode createArchiveNodeWithAuthentication(final String name) .build()); } + public PantheonNode createArchiveNodeWithAuthenticationOverWebSocket(final String name) + throws IOException, URISyntaxException { + return create( + new PantheonFactoryConfigurationBuilder() + .setName(name) + .webSocketEnabled() + .webSocketAuthenticationEnabled() + .build()); + } + public PantheonNode createNodeWithP2pDisabled(final String name) throws IOException { return create( new PantheonFactoryConfigurationBuilder() diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java new file mode 100644 index 0000000000..1e5ed52677 --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/jsonrpc/WebsocketServiceLoginAcceptanceTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.tests.acceptance.jsonrpc; + +import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import java.io.IOException; +import java.net.URISyntaxException; + +import org.junit.Before; +import org.junit.Test; + +public class WebsocketServiceLoginAcceptanceTest extends AcceptanceTestBase { + private PantheonNode node; + + @Before + public void setUp() throws IOException, URISyntaxException { + node = pantheon.createArchiveNodeWithAuthenticationOverWebSocket("node1"); + cluster.start(node); + node.useWebSocketsForJsonRpc(); + } + + @Test + public void shouldFailLoginWithWrongCredentials() { + node.verify(login.loginFails("user", "badpassword")); + } + + @Test + public void shouldSucceedLoginWithCorrectCredentials() { + node.verify(login.loginSucceeds("user", "pegasys")); + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java index 170227c718..c68c69a97c 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/authentication/AuthenticationService.java @@ -13,12 +13,14 @@ package tech.pegasys.pantheon.ethereum.jsonrpc.authentication; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Optional; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import io.netty.handler.codec.http.HttpResponseStatus; @@ -60,12 +62,16 @@ private AuthenticationService( */ public static Optional create( final Vertx vertx, final JsonRpcConfiguration config) { - final Optional jwtAuthOptions = makeJwtAuthOptions(config); + final Optional jwtAuthOptions = + makeJwtAuthOptions( + config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); if (!jwtAuthOptions.isPresent()) { return Optional.empty(); } - final Optional credentialAuthProvider = makeCredentialAuthProvider(vertx, config); + final Optional credentialAuthProvider = + makeCredentialAuthProvider( + vertx, config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); if (!credentialAuthProvider.isPresent()) { return Optional.empty(); } @@ -77,8 +83,42 @@ public static Optional create( credentialAuthProvider.get())); } - private static Optional makeJwtAuthOptions(final JsonRpcConfiguration config) { - if (config.isAuthenticationEnabled() && config.getAuthenticationCredentialsFile() != null) { + /** + * Creates a ready for use set of authentication providers if authentication is configured to be + * on + * + * @param vertx The vertx instance that will be providing requests that this set of authentication + * providers will be handling + * @param config The {{@link JsonRpcConfiguration}} that describes this rpc setup + * @return Optionally an authentication service. If empty then authentication isn't to be enabled + * on this service + */ + public static Optional create( + final Vertx vertx, final WebSocketConfiguration config) { + final Optional jwtAuthOptions = + makeJwtAuthOptions( + config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); + if (!jwtAuthOptions.isPresent()) { + return Optional.empty(); + } + + final Optional credentialAuthProvider = + makeCredentialAuthProvider( + vertx, config.isAuthenticationEnabled(), config.getAuthenticationCredentialsFile()); + if (!credentialAuthProvider.isPresent()) { + return Optional.empty(); + } + + return Optional.of( + new AuthenticationService( + jwtAuthOptions.map(o -> JWTAuth.create(vertx, o)).get(), + jwtAuthOptions.get(), + credentialAuthProvider.get())); + } + + private static Optional makeJwtAuthOptions( + final boolean authenticationEnabled, @Nullable final String authenticationCredentialsFile) { + if (authenticationEnabled && authenticationCredentialsFile != null) { final KeyPairGenerator keyGenerator; try { keyGenerator = KeyPairGenerator.getInstance("RSA"); @@ -107,12 +147,12 @@ private static Optional makeJwtAuthOptions(final JsonRpcConfigur } private static Optional makeCredentialAuthProvider( - final Vertx vertx, final JsonRpcConfiguration config) { - if (config.isAuthenticationEnabled() && config.getAuthenticationCredentialsFile() != null) { + final Vertx vertx, + final boolean authenticationEnabled, + @Nullable final String authenticationCredentialsFile) { + if (authenticationEnabled && authenticationCredentialsFile != null) { return Optional.of( - new TomlAuthOptions() - .setTomlPath(config.getAuthenticationCredentialsFile()) - .createProvider(vertx)); + new TomlAuthOptions().setTomlPath(authenticationCredentialsFile).createProvider(vertx)); } else { return Optional.empty(); } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java index 2bdc0033c9..7c2795ca1b 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java @@ -35,6 +35,8 @@ public class WebSocketConfiguration { private String host; private Collection rpcApis; private long refreshDelay; + private boolean authenticationEnabled = false; + private String authenticationCredentialsFile; public static WebSocketConfiguration createDefault() { final WebSocketConfiguration config = new WebSocketConfiguration(); @@ -92,6 +94,8 @@ public String toString() { .add("port", port) .add("host", host) .add("rpcApis", rpcApis) + .add("authenticationEnabled", authenticationEnabled) + .add("authenticationCredentialsFile", authenticationCredentialsFile) .toString(); } @@ -122,4 +126,20 @@ public void setRefreshDelay(final long refreshDelay) { public long getRefreshDelay() { return refreshDelay; } + + public boolean isAuthenticationEnabled() { + return authenticationEnabled; + } + + public void setAuthenticationEnabled(final boolean authenticationEnabled) { + this.authenticationEnabled = authenticationEnabled; + } + + public void setAuthenticationCredentialsFile(final String authenticationCredentialsFile) { + this.authenticationCredentialsFile = authenticationCredentialsFile; + } + + public String getAuthenticationCredentialsFile() { + return authenticationCredentialsFile; + } } diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java index 9129d8f17c..b8cfde1e0a 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java @@ -12,11 +12,14 @@ */ package tech.pegasys.pantheon.ethereum.jsonrpc.websocket; +import tech.pegasys.pantheon.ethereum.jsonrpc.authentication.AuthenticationService; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; import java.net.InetSocketAddress; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import com.google.common.annotations.VisibleForTesting; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -25,6 +28,8 @@ import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.ServerWebSocket; import io.vertx.core.net.SocketAddress; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -33,6 +38,7 @@ public class WebSocketService { private static final Logger LOG = LogManager.getLogger(); private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0); + private static final String APPLICATION_JSON = "application/json"; private final Vertx vertx; private final WebSocketConfiguration configuration; @@ -40,13 +46,28 @@ public class WebSocketService { private HttpServer httpServer; + @VisibleForTesting public final Optional authenticationService; + public WebSocketService( final Vertx vertx, final WebSocketConfiguration configuration, final WebSocketRequestHandler websocketRequestHandler) { + this( + vertx, + configuration, + websocketRequestHandler, + AuthenticationService.create(vertx, configuration)); + } + + private WebSocketService( + final Vertx vertx, + final WebSocketConfiguration configuration, + final WebSocketRequestHandler websocketRequestHandler, + final Optional authenticationService) { this.vertx = vertx; this.configuration = configuration; this.websocketRequestHandler = websocketRequestHandler; + this.authenticationService = authenticationService; } public CompletableFuture start() { @@ -106,8 +127,28 @@ private Handler websocketHandler() { } private Handler httpHandler() { - return http -> - http.response().setStatusCode(400).end("Websocket endpoint can't handle HTTP requests"); + final Router router = Router.router(vertx); + if (authenticationService.isPresent()) { + router.route("/login").handler(BodyHandler.create()); + router + .post("/login") + .produces(APPLICATION_JSON) + .handler(authenticationService.get()::handleLogin); + } else { + router + .post("/login") + .produces(APPLICATION_JSON) + .handler(AuthenticationService::handleDisabledLogin); + } + + router + .route() + .handler( + http -> + http.response() + .setStatusCode(400) + .end("Websocket endpoint can't handle HTTP requests")); + return router; } private Handler> startHandler(final CompletableFuture resultFuture) { diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceLoginTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceLoginTest.java new file mode 100644 index 0000000000..9f87fb04d4 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceLoginTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.ethereum.jsonrpc.websocket; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketMethodsFactory; +import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; + +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class WebSocketServiceLoginTest { + private static final int VERTX_AWAIT_TIMEOUT_MILLIS = 10000; + + private Vertx vertx; + private WebSocketConfiguration websocketConfiguration; + private WebSocketRequestHandler webSocketRequestHandlerSpy; + private WebSocketService websocketService; + private HttpClient httpClient; + + @Before + public void before() throws URISyntaxException { + vertx = Vertx.vertx(); + + final String authTomlPath = + Paths.get(ClassLoader.getSystemResource("JsonRpcHttpService/auth.toml").toURI()) + .toAbsolutePath() + .toString(); + + websocketConfiguration = WebSocketConfiguration.createDefault(); + websocketConfiguration.setPort(0); + websocketConfiguration.setAuthenticationEnabled(true); + websocketConfiguration.setAuthenticationCredentialsFile(authTomlPath); + + final Map websocketMethods = + new WebSocketMethodsFactory(new SubscriptionManager(), new HashMap<>()).methods(); + webSocketRequestHandlerSpy = spy(new WebSocketRequestHandler(vertx, websocketMethods)); + + websocketService = + new WebSocketService(vertx, websocketConfiguration, webSocketRequestHandlerSpy); + websocketService.start().join(); + + websocketConfiguration.setPort(websocketService.socketAddress().getPort()); + + final HttpClientOptions httpClientOptions = + new HttpClientOptions() + .setDefaultHost(websocketConfiguration.getHost()) + .setDefaultPort(websocketConfiguration.getPort()); + + httpClient = vertx.createHttpClient(httpClientOptions); + } + + @After + public void after() { + reset(webSocketRequestHandlerSpy); + websocketService.stop(); + } + + @Test + public void loginWithBadCredentials() { + final HttpClientRequest request = + httpClient.post( + websocketConfiguration.getPort(), + websocketConfiguration.getHost(), + "/login", + response -> { + assertThat(response.statusCode()).isEqualTo(401); + assertThat(response.statusMessage()).isEqualTo("Unauthorized"); + }); + request.putHeader("Content-Type", "application/json; charset=utf-8"); + request.end("{\"username\":\"user\",\"password\":\"pass\"}"); + } + + @Test + public void loginWithGoodCredentials() { + final HttpClientRequest request = + httpClient.post( + websocketConfiguration.getPort(), + websocketConfiguration.getHost(), + "/login", + response -> { + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.statusMessage()).isEqualTo("OK"); + assertThat(response.getHeader("Content-Type")).isNotNull(); + assertThat(response.getHeader("Content-Type")).isEqualTo("application/json"); + response.bodyHandler( + buffer -> { + final String body = buffer.toString(); + assertThat(body).isNotBlank(); + + final JsonObject respBody = new JsonObject(body); + final String token = respBody.getString("token"); + assertThat(token).isNotNull(); + + websocketService + .authenticationService + .get() + .getJwtAuthProvider() + .authenticate( + new JsonObject().put("jwt", token), + (r) -> { + assertThat(r.succeeded()).isTrue(); + final User user = r.result(); + user.isAuthorized( + "noauths", + (authed) -> { + assertThat(authed.succeeded()).isTrue(); + assertThat(authed.result()).isFalse(); + }); + user.isAuthorized( + "fakePermission", + (authed) -> { + assertThat(authed.succeeded()).isTrue(); + assertThat(authed.result()).isTrue(); + }); + }); + }); + }); + request.putHeader("Content-Type", "application/json; charset=utf-8"); + request.end("{\"username\":\"user\",\"password\":\"pegasys\"}"); + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java index f190789ac9..b316e89f14 100644 --- a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon.ethereum.jsonrpc.websocket; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; @@ -27,6 +28,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.WebSocketBase; import io.vertx.ext.unit.Async; @@ -158,4 +160,19 @@ public void websocketServiceMustReturnErrorOnHttpRequest(final TestContext conte async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); } + + @Test + public void handleLoginRequestWithAuthDisabled() { + final HttpClientRequest request = + httpClient.post( + websocketConfiguration.getPort(), + websocketConfiguration.getHost(), + "/login", + response -> { + assertThat(response.statusCode()).isEqualTo(400); + assertThat(response.statusMessage()).isEqualTo("Authentication not enabled"); + }); + request.putHeader("Content-Type", "application/json; charset=utf-8"); + request.end("{\"username\":\"user\",\"password\":\"pass\"}"); + } } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index 4f8670db5a..b093597b2c 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -277,7 +277,7 @@ public static class RpcApisConversionException extends Exception { @Option( names = {"--rpc-http-authentication-credentials-file"}, - paramLabel = MANDATORY_HOST_FORMAT_HELP, + paramLabel = MANDATORY_FILE_FORMAT_HELP, description = "Storage file for rpc http authentication credentials (default: ${DEFAULT-VALUE})", arity = "1", @@ -337,6 +337,22 @@ private Long configureRefreshDelay(final Long refreshDelay) { return refreshDelay; } + @Option( + names = {"--rpc-ws-authentication-enabled"}, + description = + "Set if the websocket JSON-RPC service should require authentication (default: ${DEFAULT-VALUE})", + hidden = true) + private final Boolean isRpcWsAuthenticationEnabled = false; + + @Option( + names = {"--rpc-ws-authentication-credentials-file"}, + paramLabel = MANDATORY_FILE_FORMAT_HELP, + description = + "Storage file for rpc websocket authentication credentials (default: ${DEFAULT-VALUE})", + arity = "1", + hidden = true) + private String rpcWsAuthenticationCredentialsFile = null; + @Option( names = {"--metrics-enabled"}, description = "Set if the metrics exporter should be started (default: ${DEFAULT-VALUE})") @@ -677,7 +693,16 @@ private WebSocketConfiguration webSocketConfiguration() { "--rpc-ws-apis", "--rpc-ws-refresh-delay", "--rpc-ws-host", - "--rpc-ws-port")); + "--rpc-ws-port", + "--rpc-ws-authentication-enabled", + "--rpc-ws-authentication-credentials-file")); + + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-ws-authentication-enabled", + !isRpcWsAuthenticationEnabled, + Collections.singletonList("--rpc-ws-authentication-credentials-file")); final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); webSocketConfiguration.setEnabled(isRpcWsEnabled); @@ -685,6 +710,8 @@ private WebSocketConfiguration webSocketConfiguration() { webSocketConfiguration.setPort(rpcWsPort); webSocketConfiguration.setRpcApis(rpcWsApis); webSocketConfiguration.setRefreshDelay(rpcWsRefreshDelay); + webSocketConfiguration.setAuthenticationEnabled(isRpcWsAuthenticationEnabled); + webSocketConfiguration.setAuthenticationCredentialsFile(rpcWsAuthenticationCredentialsFile); return webSocketConfiguration; } diff --git a/pantheon/src/test/resources/everything_config.toml b/pantheon/src/test/resources/everything_config.toml index 776f351112..ceeb99e7ad 100644 --- a/pantheon/src/test/resources/everything_config.toml +++ b/pantheon/src/test/resources/everything_config.toml @@ -51,6 +51,8 @@ rpc-ws-apis=["DEBUG","ETH"] rpc-ws-host="9.10.11.12" rpc-ws-port=9101 rpc-ws-refresh-delay=500 +rpc-ws-authentication-enabled=false +rpc-ws-authentication-credentials-file="none" # Prometheus Metrics Endpoint metrics-enabled=false