[Refactor] - Extract websocket options to a new class (#6461)

Signed-off-by: Gabriel-Trintinalia <gabriel.trintinalia@consensys.net>
pull/6466/head
Gabriel-Trintinalia 10 months ago committed by GitHub
parent 11b6c31272
commit 317d2ac6f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 209
      besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
  2. 266
      besu/src/main/java/org/hyperledger/besu/cli/options/stable/RpcWebsocketOptions.java
  3. 236
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  4. 29
      besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java
  5. 27
      besu/src/test/java/org/hyperledger/besu/cli/options/AbstractCLIOptionsTest.java
  6. 249
      besu/src/test/java/org/hyperledger/besu/cli/options/RpcWebsocketOptionsTest.java

@ -31,7 +31,6 @@ import static org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration.DEF
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.DEFAULT_RPC_APIS;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.VALID_APIS;
import static org.hyperledger.besu.ethereum.api.jsonrpc.authentication.EngineAuthService.EPHEMERAL_JWT_FILE;
import static org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration.DEFAULT_WEBSOCKET_PORT;
import static org.hyperledger.besu.metrics.BesuMetricCategory.DEFAULT_METRIC_CATEGORIES;
import static org.hyperledger.besu.metrics.MetricsProtocol.PROMETHEUS;
import static org.hyperledger.besu.metrics.prometheus.MetricsConfiguration.DEFAULT_METRICS_PORT;
@ -61,6 +60,7 @@ import org.hyperledger.besu.cli.options.stable.EthstatsOptions;
import org.hyperledger.besu.cli.options.stable.LoggingLevelOption;
import org.hyperledger.besu.cli.options.stable.NodePrivateKeyFileOption;
import org.hyperledger.besu.cli.options.stable.P2PTLSConfigOptions;
import org.hyperledger.besu.cli.options.stable.RpcWebsocketOptions;
import org.hyperledger.besu.cli.options.unstable.ChainPruningOptions;
import org.hyperledger.besu.cli.options.unstable.DnsOptions;
import org.hyperledger.besu.cli.options.unstable.EthProtocolOptions;
@ -806,93 +806,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
// JSON-RPC Websocket Options
@CommandLine.ArgGroup(validate = false, heading = "@|bold JSON-RPC Websocket Options|@%n")
JsonRPCWebsocketOptionGroup jsonRPCWebsocketOptionGroup = new JsonRPCWebsocketOptionGroup();
static class JsonRPCWebsocketOptionGroup {
@Option(
names = {"--rpc-ws-authentication-jwt-algorithm"},
description =
"Encryption algorithm used for Websockets JWT public key. Possible values are ${COMPLETION-CANDIDATES}"
+ " (default: ${DEFAULT-VALUE})",
arity = "1")
private final JwtAlgorithm rpcWebsocketsAuthenticationAlgorithm = DEFAULT_JWT_ALGORITHM;
@Option(
names = {"--rpc-ws-enabled"},
description = "Set to start the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
private final Boolean isRpcWsEnabled = false;
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@Option(
names = {"--rpc-ws-host"},
paramLabel = MANDATORY_HOST_FORMAT_HELP,
description =
"Host for JSON-RPC WebSocket service to listen on (default: ${DEFAULT-VALUE})",
arity = "1")
private String rpcWsHost;
@Option(
names = {"--rpc-ws-port"},
paramLabel = MANDATORY_PORT_FORMAT_HELP,
description =
"Port for JSON-RPC WebSocket service to listen on (default: ${DEFAULT-VALUE})",
arity = "1")
private final Integer rpcWsPort = DEFAULT_WEBSOCKET_PORT;
@Option(
names = {"--rpc-ws-max-frame-size"},
description =
"Maximum size in bytes for JSON-RPC WebSocket frames (default: ${DEFAULT-VALUE}). If this limit is exceeded, the websocket will be disconnected.",
arity = "1")
private final Integer rpcWsMaxFrameSize = DEFAULT_WS_MAX_FRAME_SIZE;
@Option(
names = {"--rpc-ws-max-active-connections"},
description =
"Maximum number of WebSocket connections allowed for JSON-RPC (default: ${DEFAULT-VALUE}). Once this limit is reached, incoming connections will be rejected.",
arity = "1")
private final Integer rpcWsMaxConnections = DEFAULT_WS_MAX_CONNECTIONS;
@Option(
names = {"--rpc-ws-api", "--rpc-ws-apis"},
paramLabel = "<api name>",
split = " {0,1}, {0,1}",
arity = "1..*",
description =
"Comma separated list of APIs to enable on JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
private final List<String> rpcWsApis = DEFAULT_RPC_APIS;
@Option(
names = {"--rpc-ws-api-methods-no-auth", "--rpc-ws-api-method-no-auth"},
paramLabel = "<api name>",
split = " {0,1}, {0,1}",
arity = "1..*",
description =
"Comma separated list of RPC methods to exclude from RPC authentication services, RPC WebSocket authentication must be enabled")
private final List<String> rpcWsApiMethodsNoAuth = new ArrayList<String>();
@Option(
names = {"--rpc-ws-authentication-enabled"},
description =
"Require authentication for the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
private final Boolean isRpcWsAuthenticationEnabled = false;
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@CommandLine.Option(
names = {"--rpc-ws-authentication-credentials-file"},
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description =
"Storage file for JSON-RPC WebSocket authentication credentials (default: ${DEFAULT-VALUE})",
arity = "1")
private String rpcWsAuthenticationCredentialsFile = null;
@CommandLine.Option(
names = {"--rpc-ws-authentication-jwt-public-key-file"},
paramLabel = MANDATORY_FILE_FORMAT_HELP,
description = "JWT public key file for JSON-RPC WebSocket authentication",
arity = "1")
private final File rpcWsAuthenticationPublicKeyFile = null;
}
RpcWebsocketOptions rpcWebsocketOptions = new RpcWebsocketOptions();
// Privacy Options Group
@CommandLine.ArgGroup(validate = false, heading = "@|bold Privacy Options|@%n")
@ -1820,6 +1734,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
validateDnsOptionsParams();
ensureValidPeerBoundParams();
validateRpcOptionsParams();
validateRpcWsOptions();
validateChainDataPruningParams();
validatePostMergeCheckpointBlockRequirements();
validateTransactionPoolOptions();
@ -1962,15 +1877,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
+ invalidHttpApis.toString());
}
if (!jsonRPCWebsocketOptionGroup.rpcWsApis.stream().allMatch(configuredApis)) {
final List<String> invalidWsApis =
new ArrayList<String>(jsonRPCWebsocketOptionGroup.rpcWsApis);
invalidWsApis.removeAll(VALID_APIS);
throw new ParameterException(
this.commandLine,
"Invalid value for option '--rpc-ws-api': invalid entries found " + invalidWsApis);
}
final boolean validHttpApiMethods =
jsonRPCHttpOptionGroup.rpcHttpApiMethodsNoAuth.stream()
.allMatch(RpcMethod::rpcMethodExists);
@ -1980,16 +1886,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
this.commandLine,
"Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods");
}
}
final boolean validWsApiMethods =
jsonRPCWebsocketOptionGroup.rpcWsApiMethodsNoAuth.stream()
.allMatch(RpcMethod::rpcMethodExists);
if (!validWsApiMethods) {
throw new ParameterException(
this.commandLine,
"Invalid value for option '--rpc-ws-api-methods-no-auth', options must be valid RPC methods");
}
private void validateRpcWsOptions() {
final Predicate<String> configuredApis =
apiName ->
Arrays.stream(RpcApis.values())
.anyMatch(builtInApi -> apiName.equals(builtInApi.name()))
|| rpcEndpointServiceImpl.hasNamespace(apiName);
rpcWebsocketOptions.validate(logger, commandLine, configuredApis);
}
private void validateChainDataPruningParams() {
@ -2092,10 +1997,10 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
p2pTLSConfiguration = p2pTLSConfigOptions.p2pTLSConfiguration(commandLine);
graphQLConfiguration = graphQLConfiguration();
webSocketConfiguration =
webSocketConfiguration(
jsonRPCWebsocketOptionGroup.rpcWsPort,
jsonRPCWebsocketOptionGroup.rpcWsApis,
hostsAllowlist);
rpcWebsocketOptions.webSocketConfiguration(
hostsAllowlist,
p2PDiscoveryOptionGroup.autoDiscoverDefaultIP().getHostAddress(),
unstableRPCOptions.getWsTimeoutSec());
jsonRpcIpcConfiguration =
jsonRpcIpcConfiguration(
unstableIpcOptions.isEnabled(),
@ -2464,71 +2369,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
return jsonRPCHttpOptionGroup.isRpcHttpEnabled && jsonRPCHttpOptionGroup.isRpcHttpTlsEnabled;
}
private WebSocketConfiguration webSocketConfiguration(
final Integer listenPort, final List<String> apiGroups, final List<String> allowCallsFrom) {
CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-ws-enabled",
!jsonRPCWebsocketOptionGroup.isRpcWsEnabled,
asList(
"--rpc-ws-api",
"--rpc-ws-apis",
"--rpc-ws-api-method-no-auth",
"--rpc-ws-api-methods-no-auth",
"--rpc-ws-host",
"--rpc-ws-port",
"--rpc-ws-max-frame-size",
"--rpc-ws-max-active-connections",
"--rpc-ws-authentication-enabled",
"--rpc-ws-authentication-credentials-file",
"--rpc-ws-authentication-public-key-file",
"--rpc-ws-authentication-jwt-algorithm"));
if (jsonRPCWebsocketOptionGroup.isRpcWsAuthenticationEnabled) {
CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-ws-authentication-public-key-file",
jsonRPCWebsocketOptionGroup.rpcWsAuthenticationPublicKeyFile == null,
asList("--rpc-ws-authentication-jwt-algorithm"));
}
if (jsonRPCWebsocketOptionGroup.isRpcWsAuthenticationEnabled
&& rpcWsAuthenticationCredentialsFile() == null
&& jsonRPCWebsocketOptionGroup.rpcWsAuthenticationPublicKeyFile == null) {
throw new ParameterException(
commandLine,
"Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file");
}
final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
webSocketConfiguration.setEnabled(jsonRPCWebsocketOptionGroup.isRpcWsEnabled);
webSocketConfiguration.setHost(
Strings.isNullOrEmpty(jsonRPCWebsocketOptionGroup.rpcWsHost)
? p2PDiscoveryOptionGroup.autoDiscoverDefaultIP().getHostAddress()
: jsonRPCWebsocketOptionGroup.rpcWsHost);
webSocketConfiguration.setPort(listenPort);
webSocketConfiguration.setMaxFrameSize(jsonRPCWebsocketOptionGroup.rpcWsMaxFrameSize);
webSocketConfiguration.setMaxActiveConnections(jsonRPCWebsocketOptionGroup.rpcWsMaxConnections);
webSocketConfiguration.setRpcApis(apiGroups);
webSocketConfiguration.setRpcApisNoAuth(
jsonRPCWebsocketOptionGroup.rpcWsApiMethodsNoAuth.stream()
.distinct()
.collect(Collectors.toList()));
webSocketConfiguration.setAuthenticationEnabled(
jsonRPCWebsocketOptionGroup.isRpcWsAuthenticationEnabled);
webSocketConfiguration.setAuthenticationCredentialsFile(rpcWsAuthenticationCredentialsFile());
webSocketConfiguration.setHostsAllowlist(allowCallsFrom);
webSocketConfiguration.setAuthenticationPublicKeyFile(
jsonRPCWebsocketOptionGroup.rpcWsAuthenticationPublicKeyFile);
webSocketConfiguration.setAuthenticationAlgorithm(
jsonRPCWebsocketOptionGroup.rpcWebsocketsAuthenticationAlgorithm);
webSocketConfiguration.setTimeoutSec(unstableRPCOptions.getWsTimeoutSec());
return webSocketConfiguration;
}
private ApiConfiguration apiConfiguration() {
checkApiOptionsDependencies();
var builder =
@ -2611,7 +2451,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private Optional<PermissioningConfiguration> permissioningConfiguration() throws Exception {
if (!(localPermissionsEnabled() || contractPermissionsEnabled())) {
if (jsonRPCHttpOptionGroup.rpcHttpApis.contains(RpcApis.PERM.name())
|| jsonRPCWebsocketOptionGroup.rpcWsApis.contains(RpcApis.PERM.name())) {
|| rpcWebsocketOptions.getRpcWsApis().contains(RpcApis.PERM.name())) {
logger.warn(
"Permissions are disabled. Cannot enable PERM APIs when not using Permissions.");
}
@ -2814,9 +2654,9 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private boolean anyPrivacyApiEnabled() {
return jsonRPCHttpOptionGroup.rpcHttpApis.contains(RpcApis.EEA.name())
|| jsonRPCWebsocketOptionGroup.rpcWsApis.contains(RpcApis.EEA.name())
|| rpcWebsocketOptions.getRpcWsApis().contains(RpcApis.EEA.name())
|| jsonRPCHttpOptionGroup.rpcHttpApis.contains(RpcApis.PRIV.name())
|| jsonRPCWebsocketOptionGroup.rpcWsApis.contains(RpcApis.PRIV.name());
|| rpcWebsocketOptions.getRpcWsApis().contains(RpcApis.PRIV.name());
}
private PrivacyKeyValueStorageProvider privacyKeyStorageProvider(final String name) {
@ -3191,15 +3031,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
return filename;
}
private String rpcWsAuthenticationCredentialsFile() {
final String filename = jsonRPCWebsocketOptionGroup.rpcWsAuthenticationCredentialsFile;
if (filename != null) {
RpcAuthFileValidator.validate(commandLine, filename, "WS");
}
return filename;
}
private String getDefaultPermissioningFilePath() {
return dataDir()
+ System.getProperty("file.separator")
@ -3337,9 +3168,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
jsonRPCHttpOptionGroup.rpcHttpPort,
jsonRPCHttpOptionGroup.isRpcHttpEnabled);
addPortIfEnabled(
effectivePorts,
jsonRPCWebsocketOptionGroup.rpcWsPort,
jsonRPCWebsocketOptionGroup.isRpcWsEnabled);
effectivePorts, rpcWebsocketOptions.getRpcWsPort(), rpcWebsocketOptions.isRpcWsEnabled());
addPortIfEnabled(effectivePorts, engineRPCOptionGroup.engineRpcPort, isEngineApiEnabled());
addPortIfEnabled(
effectivePorts, metricsOptionGroup.metricsPort, metricsOptionGroup.isMetricsEnabled);

@ -0,0 +1,266 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.cli.options.stable;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.DEFAULT_RPC_APIS;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.VALID_APIS;
import static org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration.DEFAULT_WEBSOCKET_PORT;
import org.hyperledger.besu.cli.DefaultCommandValues;
import org.hyperledger.besu.cli.custom.RpcAuthFileValidator;
import org.hyperledger.besu.cli.util.CommandLineUtils;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm;
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import picocli.CommandLine;
/** This class represents the WebSocket options for the RPC. */
public class RpcWebsocketOptions {
@CommandLine.Option(
names = {"--rpc-ws-authentication-jwt-algorithm"},
description =
"Encryption algorithm used for Websockets JWT public key. Possible values are ${COMPLETION-CANDIDATES}"
+ " (default: ${DEFAULT-VALUE})",
arity = "1")
private final JwtAlgorithm rpcWebsocketsAuthenticationAlgorithm =
DefaultCommandValues.DEFAULT_JWT_ALGORITHM;
@CommandLine.Option(
names = {"--rpc-ws-enabled"},
description = "Set to start the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
private final Boolean isRpcWsEnabled = false;
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@CommandLine.Option(
names = {"--rpc-ws-host"},
paramLabel = DefaultCommandValues.MANDATORY_HOST_FORMAT_HELP,
description = "Host for JSON-RPC WebSocket service to listen on (default: ${DEFAULT-VALUE})",
arity = "1")
private String rpcWsHost;
@CommandLine.Option(
names = {"--rpc-ws-port"},
paramLabel = DefaultCommandValues.MANDATORY_PORT_FORMAT_HELP,
description = "Port for JSON-RPC WebSocket service to listen on (default: ${DEFAULT-VALUE})",
arity = "1")
private final Integer rpcWsPort = DEFAULT_WEBSOCKET_PORT;
@CommandLine.Option(
names = {"--rpc-ws-max-frame-size"},
description =
"Maximum size in bytes for JSON-RPC WebSocket frames (default: ${DEFAULT-VALUE}). If this limit is exceeded, the websocket will be disconnected.",
arity = "1")
private final Integer rpcWsMaxFrameSize = DefaultCommandValues.DEFAULT_WS_MAX_FRAME_SIZE;
@CommandLine.Option(
names = {"--rpc-ws-max-active-connections"},
description =
"Maximum number of WebSocket connections allowed for JSON-RPC (default: ${DEFAULT-VALUE}). Once this limit is reached, incoming connections will be rejected.",
arity = "1")
private final Integer rpcWsMaxConnections = DefaultCommandValues.DEFAULT_WS_MAX_CONNECTIONS;
@CommandLine.Option(
names = {"--rpc-ws-api", "--rpc-ws-apis"},
paramLabel = "<api name>",
split = " {0,1}, {0,1}",
arity = "1..*",
description =
"Comma separated list of APIs to enable on JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
private final List<String> rpcWsApis = DEFAULT_RPC_APIS;
@CommandLine.Option(
names = {"--rpc-ws-api-methods-no-auth", "--rpc-ws-api-method-no-auth"},
paramLabel = "<api name>",
split = " {0,1}, {0,1}",
arity = "1..*",
description =
"Comma separated list of RPC methods to exclude from RPC authentication services, RPC WebSocket authentication must be enabled")
private final List<String> rpcWsApiMethodsNoAuth = new ArrayList<String>();
@CommandLine.Option(
names = {"--rpc-ws-authentication-enabled"},
description =
"Require authentication for the JSON-RPC WebSocket service (default: ${DEFAULT-VALUE})")
private final Boolean isRpcWsAuthenticationEnabled = false;
@SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings.
@CommandLine.Option(
names = {"--rpc-ws-authentication-credentials-file"},
paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP,
description =
"Storage file for JSON-RPC WebSocket authentication credentials (default: ${DEFAULT-VALUE})",
arity = "1")
private String rpcWsAuthenticationCredentialsFile = null;
@CommandLine.Option(
names = {"--rpc-ws-authentication-jwt-public-key-file"},
paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP,
description = "JWT public key file for JSON-RPC WebSocket authentication",
arity = "1")
private final File rpcWsAuthenticationPublicKeyFile = null;
/**
* Validates the WebSocket options.
*
* @param logger Logger instance
* @param commandLine CommandLine instance
* @param configuredApis Predicate for configured APIs
*/
public void validate(
final Logger logger, final CommandLine commandLine, final Predicate<String> configuredApis) {
checkOptionDependencies(logger, commandLine);
if (!rpcWsApis.stream().allMatch(configuredApis)) {
final List<String> invalidWsApis = new ArrayList<>(rpcWsApis);
invalidWsApis.removeAll(VALID_APIS);
throw new CommandLine.ParameterException(
commandLine,
"Invalid value for option '--rpc-ws-api': invalid entries found " + invalidWsApis);
}
final boolean validWsApiMethods =
rpcWsApiMethodsNoAuth.stream().allMatch(RpcMethod::rpcMethodExists);
if (!validWsApiMethods) {
throw new CommandLine.ParameterException(
commandLine,
"Invalid value for option '--rpc-ws-api-methods-no-auth', options must be valid RPC methods");
}
if (isRpcWsAuthenticationEnabled
&& rpcWsAuthenticationCredentialsFile(commandLine) == null
&& rpcWsAuthenticationPublicKeyFile == null) {
throw new CommandLine.ParameterException(
commandLine,
"Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file");
}
}
/**
* Checks the dependencies of the WebSocket options.
*
* @param logger Logger instance
* @param commandLine CommandLine instance
*/
private void checkOptionDependencies(final Logger logger, final CommandLine commandLine) {
CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-ws-enabled",
!isRpcWsEnabled,
List.of(
"--rpc-ws-api",
"--rpc-ws-apis",
"--rpc-ws-api-method-no-auth",
"--rpc-ws-api-methods-no-auth",
"--rpc-ws-host",
"--rpc-ws-port",
"--rpc-ws-max-frame-size",
"--rpc-ws-max-active-connections",
"--rpc-ws-authentication-enabled",
"--rpc-ws-authentication-credentials-file",
"--rpc-ws-authentication-public-key-file",
"--rpc-ws-authentication-jwt-algorithm"));
if (isRpcWsAuthenticationEnabled) {
CommandLineUtils.checkOptionDependencies(
logger,
commandLine,
"--rpc-ws-authentication-public-key-file",
rpcWsAuthenticationPublicKeyFile == null,
List.of("--rpc-ws-authentication-jwt-algorithm"));
}
}
/**
* Creates a WebSocket configuration based on the WebSocket options.
*
* @param hostsAllowlist List of allowed hosts
* @param defaultHostAddress Default host address
* @param wsTimoutSec WebSocket timeout in seconds
* @return WebSocketConfiguration instance
*/
public WebSocketConfiguration webSocketConfiguration(
final List<String> hostsAllowlist, final String defaultHostAddress, final Long wsTimoutSec) {
final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
webSocketConfiguration.setEnabled(isRpcWsEnabled);
webSocketConfiguration.setHost(
Strings.isNullOrEmpty(rpcWsHost) ? defaultHostAddress : rpcWsHost);
webSocketConfiguration.setPort(rpcWsPort);
webSocketConfiguration.setMaxFrameSize(rpcWsMaxFrameSize);
webSocketConfiguration.setMaxActiveConnections(rpcWsMaxConnections);
webSocketConfiguration.setRpcApis(rpcWsApis);
webSocketConfiguration.setRpcApisNoAuth(
rpcWsApiMethodsNoAuth.stream().distinct().collect(Collectors.toList()));
webSocketConfiguration.setAuthenticationEnabled(isRpcWsAuthenticationEnabled);
webSocketConfiguration.setAuthenticationCredentialsFile(rpcWsAuthenticationCredentialsFile);
webSocketConfiguration.setHostsAllowlist(hostsAllowlist);
webSocketConfiguration.setAuthenticationPublicKeyFile(rpcWsAuthenticationPublicKeyFile);
webSocketConfiguration.setAuthenticationAlgorithm(rpcWebsocketsAuthenticationAlgorithm);
webSocketConfiguration.setTimeoutSec(wsTimoutSec);
return webSocketConfiguration;
}
/**
* Validates the authentication credentials file for the WebSocket.
*
* @param commandLine CommandLine instance
* @return Filename of the authentication credentials file
*/
private String rpcWsAuthenticationCredentialsFile(final CommandLine commandLine) {
final String filename = rpcWsAuthenticationCredentialsFile;
if (filename != null) {
RpcAuthFileValidator.validate(commandLine, filename, "WS");
}
return filename;
}
/**
* Returns the list of APIs for the WebSocket.
*
* @return List of APIs
*/
public List<String> getRpcWsApis() {
return rpcWsApis;
}
/**
* Checks if the WebSocket service is enabled.
*
* @return Boolean indicating if the WebSocket service is enabled
*/
public Boolean isRpcWsEnabled() {
return isRpcWsEnabled;
}
/**
* Returns the port for the WebSocket service.
*
* @return Port number
*/
public Integer getRpcWsPort() {
return rpcWsPort;
}
}

@ -27,7 +27,6 @@ import static org.hyperledger.besu.cli.config.NetworkName.HOLESKY;
import static org.hyperledger.besu.cli.config.NetworkName.MAINNET;
import static org.hyperledger.besu.cli.config.NetworkName.MORDOR;
import static org.hyperledger.besu.cli.config.NetworkName.SEPOLIA;
import static org.hyperledger.besu.cli.util.CommandLineUtils.DEPENDENCY_WARNING_MSG;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ENGINE;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.NET;
@ -2144,18 +2143,6 @@ public class BesuCommandTest extends CommandTestAbstract {
"Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods");
}
@Test
public void rpcWsNoAuthApiMethodsCannotBeInvalid() {
parseCommand("--rpc-ws-enabled", "--rpc-ws-api-methods-no-auth", "invalid");
Mockito.verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"Invalid value for option '--rpc-ws-api-methods-no-auth', options must be valid RPC methods");
}
@Test
public void rpcHttpOptionsRequiresServiceToBeEnabled() {
parseCommand(
@ -2301,18 +2288,6 @@ public class BesuCommandTest extends CommandTestAbstract {
.contains("Invalid value for option '--rpc-http-api': invalid entries found [BOB]");
}
@Test
public void rpcWsApisPropertyWithInvalidEntryMustDisplayError() {
parseCommand("--rpc-ws-api", "ETH,BOB,TEST");
Mockito.verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8).trim())
.contains("Invalid value for option '--rpc-ws-api': invalid entries found [BOB, TEST]");
}
@Test
public void rpcApisPropertyWithPluginNamespaceAreValid() {
@ -2394,35 +2369,6 @@ public class BesuCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsMaxFrameSizePropertyMustBeUsed() {
final int maxFrameSize = 65535;
parseCommand("--rpc-ws-max-frame-size", String.valueOf(maxFrameSize));
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getMaxFrameSize()).isEqualTo(maxFrameSize);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsMaxActiveConnectionsPropertyMustBeUsed() {
final int maxConnections = 99;
parseCommand("--rpc-ws-max-active-connections", String.valueOf(maxConnections));
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getMaxActiveConnections())
.isEqualTo(maxConnections);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcHttpTlsRequiresRpcHttpEnabled() {
parseCommand("--rpc-http-tls-enabled");
@ -3234,129 +3180,6 @@ public class BesuCommandTest extends CommandTestAbstract {
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsRpcEnabledPropertyMustBeUsed() {
parseCommand("--rpc-ws-enabled");
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().isEnabled()).isTrue();
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsOptionsRequiresServiceToBeEnabled() {
parseCommand(
"--rpc-ws-api",
"ETH,NET",
"--rpc-ws-host",
"0.0.0.0",
"--rpc-ws-port",
"1234",
"--rpc-ws-max-active-connections",
"77",
"--rpc-ws-max-frame-size",
"65535");
verifyOptionsConstraintLoggerCall(
"--rpc-ws-enabled",
"--rpc-ws-host",
"--rpc-ws-port",
"--rpc-ws-api",
"--rpc-ws-max-active-connections",
"--rpc-ws-max-frame-size");
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsOptionsRequiresServiceToBeEnabledToml() throws IOException {
final Path toml =
createTempFile(
"toml",
"rpc-ws-api=[\"ETH\", \"NET\"]\n"
+ "rpc-ws-host=\"0.0.0.0\"\n"
+ "rpc-ws-port=1234\n"
+ "rpc-ws-max-active-connections=77\n"
+ "rpc-ws-max-frame-size=65535\n");
parseCommand("--config-file", toml.toString());
verifyOptionsConstraintLoggerCall(
"--rpc-ws-enabled",
"--rpc-ws-host",
"--rpc-ws-port",
"--rpc-ws-api",
"--rpc-ws-max-active-connections",
"--rpc-ws-max-frame-size");
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsApiPropertyMustBeUsed() {
final TestBesuCommand command = parseCommand("--rpc-ws-enabled", "--rpc-ws-api", "ETH, NET");
assertThat(command).isNotNull();
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getRpcApis())
.containsExactlyInAnyOrder(ETH.name(), NET.name());
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsHostAndPortOptionMustBeUsed() {
final String host = "1.2.3.4";
final int port = 1234;
parseCommand("--rpc-ws-enabled", "--rpc-ws-host", host, "--rpc-ws-port", String.valueOf(port));
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host);
assertThat(wsRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsHostAndMayBeLocalhost() {
final String host = "localhost";
parseCommand("--rpc-ws-enabled", "--rpc-ws-host", host);
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsHostAndMayBeIPv6() {
final String host = "2600:DB8::8545";
parseCommand("--rpc-ws-enabled", "--rpc-ws-host", host);
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void metricsEnabledPropertyDefaultIsFalse() {
parseCommand();
@ -4146,31 +3969,6 @@ public class BesuCommandTest extends CommandTestAbstract {
"No Payload Provider has been provided. You must register one when enabling privacy plugin!");
}
/**
* Check logger calls
*
* <p>Here we check the calls to logger and not the result of the log line as we don't test the
* logger itself but the fact that we call it.
*
* @param dependentOptions the string representing the list of dependent options names
* @param mainOption the main option name
*/
private void verifyOptionsConstraintLoggerCall(
final String mainOption, final String... dependentOptions) {
verify(mockLogger, atLeast(1))
.warn(
stringArgumentCaptor.capture(),
stringArgumentCaptor.capture(),
stringArgumentCaptor.capture());
assertThat(stringArgumentCaptor.getAllValues().get(0)).isEqualTo(DEPENDENCY_WARNING_MSG);
for (final String option : dependentOptions) {
assertThat(stringArgumentCaptor.getAllValues().get(1)).contains(option);
}
assertThat(stringArgumentCaptor.getAllValues().get(2)).isEqualTo(mainOption);
}
/**
* Check logger calls
*
@ -4360,17 +4158,6 @@ public class BesuCommandTest extends CommandTestAbstract {
.isEqualTo(JwtAlgorithm.ES256);
}
@Test
public void webSocketAuthenticationAlgorithIsConfigured() {
parseCommand("--rpc-ws-authentication-jwt-algorithm", "ES256");
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getAuthenticationAlgorithm())
.isEqualTo(JwtAlgorithm.ES256);
}
@Test
public void httpAuthenticationPublicKeyIsConfigured() throws IOException {
final Path publicKey = Files.createTempFile("public_key", "");
@ -4394,29 +4181,6 @@ public class BesuCommandTest extends CommandTestAbstract {
"Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file");
}
@Test
public void wsAuthenticationPublicKeyIsConfigured() throws IOException {
final Path publicKey = Files.createTempFile("public_key", "");
parseCommand("--rpc-ws-authentication-jwt-public-key-file", publicKey.toString());
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath())
.isEqualTo(publicKey.toString());
}
@Test
public void wsAuthenticationWithoutRequiredConfiguredOptionsMustFail() {
parseCommand("--rpc-ws-enabled", "--rpc-ws-authentication-enabled");
verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file");
}
@Test
public void privHttpApisWithPrivacyDisabledLogsWarning() {
parseCommand("--privacy-enabled=false", "--rpc-http-api", "PRIV", "--rpc-http-enabled");

@ -15,14 +15,18 @@
package org.hyperledger.besu.cli;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.cli.util.CommandLineUtils.DEPENDENCY_WARNING_MSG;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.Runner;
@ -687,4 +691,29 @@ public abstract class CommandTestAbstract {
protected static String escapeTomlString(final String s) {
return StringEscapeUtils.escapeJava(s);
}
/**
* Check logger calls
*
* <p>Here we check the calls to logger and not the result of the log line as we don't test the
* logger itself but the fact that we call it.
*
* @param dependentOptions the string representing the list of dependent options names
* @param mainOption the main option name
*/
protected void verifyOptionsConstraintLoggerCall(
final String mainOption, final String... dependentOptions) {
verify(mockLogger, atLeast(1))
.warn(
stringArgumentCaptor.capture(),
stringArgumentCaptor.capture(),
stringArgumentCaptor.capture());
assertThat(stringArgumentCaptor.getAllValues().get(0)).isEqualTo(DEPENDENCY_WARNING_MSG);
for (final String option : dependentOptions) {
assertThat(stringArgumentCaptor.getAllValues().get(1)).contains(option);
}
assertThat(stringArgumentCaptor.getAllValues().get(2)).isEqualTo(mainOption);
}
}

@ -16,8 +16,6 @@ package org.hyperledger.besu.cli.options;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.cli.util.CommandLineUtils.DEPENDENCY_WARNING_MSG;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.verify;
import org.hyperledger.besu.cli.CommandTestAbstract;
@ -125,29 +123,4 @@ public abstract class AbstractCLIOptionsTest<D, T extends CLIOptions<D>>
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).contains(errorMsg);
}
/**
* Check logger calls
*
* <p>Here we check the calls to logger and not the result of the log line as we don't test the
* logger itself but the fact that we call it.
*
* @param dependentOptions the string representing the list of dependent options names
* @param mainOption the main option name
*/
protected void verifyOptionsConstraintLoggerCall(
final String mainOption, final String... dependentOptions) {
verify(mockLogger, atLeast(1))
.warn(
stringArgumentCaptor.capture(),
stringArgumentCaptor.capture(),
stringArgumentCaptor.capture());
assertThat(stringArgumentCaptor.getAllValues().get(0)).isEqualTo(DEPENDENCY_WARNING_MSG);
for (final String option : dependentOptions) {
assertThat(stringArgumentCaptor.getAllValues().get(1)).contains(option);
}
assertThat(stringArgumentCaptor.getAllValues().get(2)).isEqualTo(mainOption);
}
}

@ -0,0 +1,249 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.cli.options;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH;
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.NET;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import org.hyperledger.besu.cli.CommandTestAbstract;
import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class RpcWebsocketOptionsTest extends CommandTestAbstract {
@Test
public void rpcWsApiPropertyMustBeUsed() {
final CommandTestAbstract.TestBesuCommand command =
parseCommand("--rpc-ws-enabled", "--rpc-ws-api", "ETH, NET");
assertThat(command).isNotNull();
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getRpcApis())
.containsExactlyInAnyOrder(ETH.name(), NET.name());
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsHostAndPortOptionMustBeUsed() {
final String host = "1.2.3.4";
final int port = 1234;
parseCommand("--rpc-ws-enabled", "--rpc-ws-host", host, "--rpc-ws-port", String.valueOf(port));
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host);
assertThat(wsRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsMaxFrameSizePropertyMustBeUsed() {
final int maxFrameSize = 65535;
parseCommand("--rpc-ws-max-frame-size", String.valueOf(maxFrameSize));
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getMaxFrameSize()).isEqualTo(maxFrameSize);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsMaxActiveConnectionsPropertyMustBeUsed() {
final int maxConnections = 99;
parseCommand("--rpc-ws-max-active-connections", String.valueOf(maxConnections));
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getMaxActiveConnections())
.isEqualTo(maxConnections);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsRpcEnabledPropertyMustBeUsed() {
parseCommand("--rpc-ws-enabled");
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().isEnabled()).isTrue();
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void webSocketAuthenticationAlgorithmIsConfigured() {
parseCommand("--rpc-ws-authentication-jwt-algorithm", "ES256");
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getAuthenticationAlgorithm())
.isEqualTo(JwtAlgorithm.ES256);
}
@Test
public void wsAuthenticationPublicKeyIsConfigured() throws IOException {
final Path publicKey = Files.createTempFile("public_key", "");
parseCommand("--rpc-ws-authentication-jwt-public-key-file", publicKey.toString());
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath())
.isEqualTo(publicKey.toString());
}
@Test
public void rpcWsHostAndMayBeLocalhost() {
final String host = "localhost";
parseCommand("--rpc-ws-enabled", "--rpc-ws-host", host);
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsHostAndMayBeIPv6() {
final String host = "2600:DB8::8545";
parseCommand("--rpc-ws-enabled", "--rpc-ws-host", host);
verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture());
verify(mockRunnerBuilder).build();
assertThat(wsRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsNoAuthApiMethodsCannotBeInvalid() {
parseCommand("--rpc-ws-enabled", "--rpc-ws-api-methods-no-auth", "invalid");
Mockito.verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"Invalid value for option '--rpc-ws-api-methods-no-auth', options must be valid RPC methods");
}
@Test
public void rpcWsApisPropertyWithInvalidEntryMustDisplayError() {
parseCommand("--rpc-ws-api", "ETH,BOB,TEST");
Mockito.verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8).trim())
.contains("Invalid value for option '--rpc-ws-api': invalid entries found [BOB, TEST]");
}
@Test
public void rpcWsOptionsRequiresServiceToBeEnabled() {
parseCommand(
"--rpc-ws-api",
"ETH,NET",
"--rpc-ws-host",
"0.0.0.0",
"--rpc-ws-port",
"1234",
"--rpc-ws-max-active-connections",
"77",
"--rpc-ws-max-frame-size",
"65535");
verifyOptionsConstraintLoggerCall(
"--rpc-ws-enabled",
"--rpc-ws-host",
"--rpc-ws-port",
"--rpc-ws-api",
"--rpc-ws-max-active-connections",
"--rpc-ws-max-frame-size");
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void rpcWsOptionsRequiresServiceToBeEnabledToml() throws IOException {
final Path toml =
createTempFile(
"toml",
"rpc-ws-api=[\"ETH\", \"NET\"]\n"
+ "rpc-ws-host=\"0.0.0.0\"\n"
+ "rpc-ws-port=1234\n"
+ "rpc-ws-max-active-connections=77\n"
+ "rpc-ws-max-frame-size=65535\n");
parseCommand("--config-file", toml.toString());
verifyOptionsConstraintLoggerCall(
"--rpc-ws-enabled",
"--rpc-ws-host",
"--rpc-ws-port",
"--rpc-ws-api",
"--rpc-ws-max-active-connections",
"--rpc-ws-max-frame-size");
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void wsAuthenticationWithoutRequiredConfiguredOptionsMustFail() {
parseCommand("--rpc-ws-enabled", "--rpc-ws-authentication-enabled");
verifyNoInteractions(mockRunnerBuilder);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file");
}
}
Loading…
Cancel
Save