mirror of https://github.com/hyperledger/besu
commit
36ee9397c5
@ -0,0 +1,41 @@ |
|||||||
|
/* |
||||||
|
* 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.config; |
||||||
|
|
||||||
|
/** Enum for profile names. Each profile corresponds to a configuration file. */ |
||||||
|
public enum ProfileName { |
||||||
|
/** The 'DEV' profile. Corresponds to the 'profiles/dev.toml' configuration file. */ |
||||||
|
DEV("profiles/dev.toml"); |
||||||
|
|
||||||
|
private final String configFile; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a new ProfileName. |
||||||
|
* |
||||||
|
* @param configFile the configuration file corresponding to the profile |
||||||
|
*/ |
||||||
|
ProfileName(final String configFile) { |
||||||
|
this.configFile = configFile; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the configuration file corresponding to the profile. |
||||||
|
* |
||||||
|
* @return the configuration file |
||||||
|
*/ |
||||||
|
public String getConfigFile() { |
||||||
|
return configFile; |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
/* |
||||||
|
* 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.util; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import picocli.CommandLine; |
||||||
|
|
||||||
|
/** |
||||||
|
* Abstract class for finding configuration resources. This class provides a common structure for |
||||||
|
* classes that need to find configuration resources based on command line options and environment |
||||||
|
* variables. |
||||||
|
* |
||||||
|
* @param <T> the type of configuration resource this finder will return |
||||||
|
*/ |
||||||
|
public abstract class AbstractConfigurationFinder<T> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the name of the configuration option. |
||||||
|
* |
||||||
|
* @return the name of the configuration option |
||||||
|
*/ |
||||||
|
protected abstract String getConfigOptionName(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the name of the environment variable for the configuration. |
||||||
|
* |
||||||
|
* @return the name of the environment variable for the configuration |
||||||
|
*/ |
||||||
|
protected abstract String getConfigEnvName(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Finds the configuration resource based on command line options and environment variables. |
||||||
|
* |
||||||
|
* @param environment the environment variables |
||||||
|
* @param parseResult the command line parse result |
||||||
|
* @return an Optional containing the configuration resource, or an empty Optional if no |
||||||
|
* configuration resource was found |
||||||
|
*/ |
||||||
|
public Optional<T> findConfiguration( |
||||||
|
final Map<String, String> environment, final CommandLine.ParseResult parseResult) { |
||||||
|
final CommandLine commandLine = parseResult.commandSpec().commandLine(); |
||||||
|
if (isConfigSpecifiedInBothSources(environment, parseResult)) { |
||||||
|
throwExceptionForBothSourcesSpecified(environment, parseResult, commandLine); |
||||||
|
} |
||||||
|
if (parseResult.hasMatchedOption(getConfigOptionName())) { |
||||||
|
return getFromOption(parseResult, commandLine); |
||||||
|
} |
||||||
|
if (environment.containsKey(getConfigEnvName())) { |
||||||
|
return getFromEnvironment(environment, commandLine); |
||||||
|
} |
||||||
|
return Optional.empty(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the configuration resource from the command line option. |
||||||
|
* |
||||||
|
* @param parseResult the command line parse result |
||||||
|
* @param commandLine the command line |
||||||
|
* @return an Optional containing the configuration resource, or an empty Optional if the |
||||||
|
* configuration resource was not specified in the command line option |
||||||
|
*/ |
||||||
|
protected abstract Optional<T> getFromOption( |
||||||
|
final CommandLine.ParseResult parseResult, final CommandLine commandLine); |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the configuration resource from the environment variable. |
||||||
|
* |
||||||
|
* @param environment the environment variables |
||||||
|
* @param commandLine the command line |
||||||
|
* @return an Optional containing the configuration resource, or an empty Optional if the |
||||||
|
* configuration resource was not specified in the environment variable |
||||||
|
*/ |
||||||
|
protected abstract Optional<T> getFromEnvironment( |
||||||
|
final Map<String, String> environment, final CommandLine commandLine); |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if the configuration resource is specified in both command line options and environment |
||||||
|
* variables. |
||||||
|
* |
||||||
|
* @param environment the environment variables |
||||||
|
* @param parseResult the command line parse result |
||||||
|
* @return true if the configuration resource is specified in both places, false otherwise |
||||||
|
*/ |
||||||
|
public boolean isConfigSpecifiedInBothSources( |
||||||
|
final Map<String, String> environment, final CommandLine.ParseResult parseResult) { |
||||||
|
return parseResult.hasMatchedOption(getConfigOptionName()) |
||||||
|
&& environment.containsKey(getConfigEnvName()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Throws an exception if the configuration resource is specified in both command line options and |
||||||
|
* environment variables. |
||||||
|
* |
||||||
|
* @param environment the environment variables |
||||||
|
* @param parseResult the command line parse result |
||||||
|
* @param commandLine the command line |
||||||
|
*/ |
||||||
|
public void throwExceptionForBothSourcesSpecified( |
||||||
|
final Map<String, String> environment, |
||||||
|
final CommandLine.ParseResult parseResult, |
||||||
|
final CommandLine commandLine) { |
||||||
|
throw new CommandLine.ParameterException( |
||||||
|
commandLine, |
||||||
|
String.format( |
||||||
|
"Both %s=%s and %s %s specified. Please specify only one.", |
||||||
|
getConfigEnvName(), |
||||||
|
getConfigOptionName(), |
||||||
|
environment.get(getConfigEnvName()), |
||||||
|
parseResult.matchedOption(getConfigOptionName()).stringValues())); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
/* |
||||||
|
* 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.util; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.cli.DefaultCommandValues.CONFIG_FILE_OPTION_NAME; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import picocli.CommandLine; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class for finding configuration files. This class extends the AbstractConfigurationFinder and |
||||||
|
* provides methods for finding configuration files based on command line options and environment |
||||||
|
* variables. |
||||||
|
*/ |
||||||
|
public class ConfigFileFinder extends AbstractConfigurationFinder<File> { |
||||||
|
private static final String CONFIG_FILE_ENV_NAME = "BESU_CONFIG_FILE"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the name of the configuration option. |
||||||
|
* |
||||||
|
* @return the name of the configuration option |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
protected String getConfigOptionName() { |
||||||
|
return CONFIG_FILE_OPTION_NAME; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the name of the environment variable for the configuration. |
||||||
|
* |
||||||
|
* @return the name of the environment variable for the configuration |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
protected String getConfigEnvName() { |
||||||
|
return CONFIG_FILE_ENV_NAME; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the configuration file from the command line option. |
||||||
|
* |
||||||
|
* @param parseResult the command line parse result |
||||||
|
* @param commandLine the command line |
||||||
|
* @return an Optional containing the configuration file, or an empty Optional if the |
||||||
|
* configuration file was not specified in the command line option |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public Optional<File> getFromOption( |
||||||
|
final CommandLine.ParseResult parseResult, final CommandLine commandLine) { |
||||||
|
final CommandLine.Model.OptionSpec configFileOption = |
||||||
|
parseResult.matchedOption(CONFIG_FILE_OPTION_NAME); |
||||||
|
try { |
||||||
|
File file = configFileOption.getter().get(); |
||||||
|
if (!file.exists()) { |
||||||
|
throw new CommandLine.ParameterException( |
||||||
|
commandLine, |
||||||
|
String.format("Unable to read TOML configuration, file not found: %s", file)); |
||||||
|
} |
||||||
|
return Optional.of(file); |
||||||
|
} catch (final Exception e) { |
||||||
|
throw new CommandLine.ParameterException(commandLine, e.getMessage(), e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the configuration file from the environment variable. |
||||||
|
* |
||||||
|
* @param environment the environment variables |
||||||
|
* @param commandLine the command line |
||||||
|
* @return an Optional containing the configuration file, or an empty Optional if the |
||||||
|
* configuration file was not specified in the environment variable |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public Optional<File> getFromEnvironment( |
||||||
|
final Map<String, String> environment, final CommandLine commandLine) { |
||||||
|
final File toml = new File(environment.get(CONFIG_FILE_ENV_NAME)); |
||||||
|
if (!toml.exists()) { |
||||||
|
throw new CommandLine.ParameterException( |
||||||
|
commandLine, |
||||||
|
String.format( |
||||||
|
"TOML file %s specified in environment variable %s not found", |
||||||
|
CONFIG_FILE_ENV_NAME, environment.get(CONFIG_FILE_ENV_NAME))); |
||||||
|
} |
||||||
|
return Optional.of(toml); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
/* |
||||||
|
* 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.util; |
||||||
|
|
||||||
|
import static org.hyperledger.besu.cli.DefaultCommandValues.PROFILE_OPTION_NAME; |
||||||
|
|
||||||
|
import org.hyperledger.besu.cli.config.ProfileName; |
||||||
|
|
||||||
|
import java.io.InputStream; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Optional; |
||||||
|
|
||||||
|
import picocli.CommandLine; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class for finding profile configurations. This class extends the AbstractConfigurationFinder and |
||||||
|
* provides methods for finding profile configurations based on command line options and environment |
||||||
|
* variables. Each profile corresponds to a TOML configuration file that contains settings for |
||||||
|
* various options. The profile to use can be specified with the '--profile' command line option or |
||||||
|
* the 'BESU_PROFILE' environment variable. |
||||||
|
*/ |
||||||
|
public class ProfileFinder extends AbstractConfigurationFinder<InputStream> { |
||||||
|
private static final String PROFILE_ENV_NAME = "BESU_PROFILE"; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected String getConfigOptionName() { |
||||||
|
return PROFILE_OPTION_NAME; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected String getConfigEnvName() { |
||||||
|
return PROFILE_ENV_NAME; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<InputStream> getFromOption( |
||||||
|
final CommandLine.ParseResult parseResult, final CommandLine commandLine) { |
||||||
|
try { |
||||||
|
return getProfile(parseResult.matchedOption(PROFILE_OPTION_NAME).getter().get(), commandLine); |
||||||
|
} catch (Exception e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Optional<InputStream> getFromEnvironment( |
||||||
|
final Map<String, String> environment, final CommandLine commandLine) { |
||||||
|
return getProfile(ProfileName.valueOf(environment.get(PROFILE_ENV_NAME)), commandLine); |
||||||
|
} |
||||||
|
|
||||||
|
private static Optional<InputStream> getProfile( |
||||||
|
final ProfileName profileName, final CommandLine commandLine) { |
||||||
|
return Optional.of(getTomlFile(commandLine, profileName.getConfigFile())); |
||||||
|
} |
||||||
|
|
||||||
|
private static InputStream getTomlFile(final CommandLine commandLine, final String file) { |
||||||
|
InputStream resourceUrl = ProfileFinder.class.getClassLoader().getResourceAsStream(file); |
||||||
|
if (resourceUrl == null) { |
||||||
|
throw new CommandLine.ParameterException( |
||||||
|
commandLine, String.format("TOML file %s not found", file)); |
||||||
|
} |
||||||
|
return resourceUrl; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,306 @@ |
|||||||
|
/* |
||||||
|
* 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; |
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8; |
||||||
|
import static java.util.Arrays.asList; |
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.hyperledger.besu.cli.config.NetworkName.DEV; |
||||||
|
import static org.hyperledger.besu.cli.config.NetworkName.MAINNET; |
||||||
|
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.ETH; |
||||||
|
import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.WEB3; |
||||||
|
import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES; |
||||||
|
import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_DISCOVERY_URL; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
|
||||||
|
import org.hyperledger.besu.cli.config.EthNetworkConfig; |
||||||
|
import org.hyperledger.besu.datatypes.Address; |
||||||
|
import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.core.ImmutableMiningParameters; |
||||||
|
import org.hyperledger.besu.ethereum.eth.sync.SyncMode; |
||||||
|
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; |
||||||
|
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURLImpl; |
||||||
|
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; |
||||||
|
import org.hyperledger.besu.plugin.data.EnodeURL; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
import java.math.BigInteger; |
||||||
|
import java.net.URL; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import com.google.common.io.Resources; |
||||||
|
import io.vertx.core.json.JsonObject; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.junit.jupiter.api.io.TempDir; |
||||||
|
import org.mockito.ArgumentCaptor; |
||||||
|
|
||||||
|
public class CascadingDefaultProviderTest extends CommandTestAbstract { |
||||||
|
private static final int GENESIS_CONFIG_TEST_CHAINID = 3141592; |
||||||
|
private static final String VALID_NODE_ID = |
||||||
|
"6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"; |
||||||
|
private static final JsonObject GENESIS_VALID_JSON = |
||||||
|
(new JsonObject()) |
||||||
|
.put("config", (new JsonObject()).put("chainId", GENESIS_CONFIG_TEST_CHAINID)); |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the default values are overridden if the key is present in the configuration file. The |
||||||
|
* test checks if the configuration file correctly overrides the default values for various |
||||||
|
* settings, such as the JSON-RPC configuration, GraphQL configuration, WebSocket configuration, |
||||||
|
* and metrics configuration. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void overrideDefaultValuesIfKeyIsPresentInConfigFile(final @TempDir File dataFolder) |
||||||
|
throws IOException { |
||||||
|
final URL configFile = this.getClass().getResource("/complete_config.toml"); |
||||||
|
final Path genesisFile = createFakeGenesisFile(GENESIS_VALID_JSON); |
||||||
|
final String updatedConfig = |
||||||
|
Resources.toString(configFile, UTF_8) |
||||||
|
.replace("/opt/besu/genesis.json", escapeTomlString(genesisFile.toString())) |
||||||
|
.replace( |
||||||
|
"data-path=\"/opt/besu\"", |
||||||
|
"data-path=\"" + escapeTomlString(dataFolder.getPath()) + "\""); |
||||||
|
|
||||||
|
final Path toml = createTempFile("toml", updatedConfig.getBytes(UTF_8)); |
||||||
|
|
||||||
|
final List<String> expectedApis = asList(ETH.name(), WEB3.name()); |
||||||
|
|
||||||
|
final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); |
||||||
|
jsonRpcConfiguration.setEnabled(false); |
||||||
|
jsonRpcConfiguration.setHost("5.6.7.8"); |
||||||
|
jsonRpcConfiguration.setPort(5678); |
||||||
|
jsonRpcConfiguration.setCorsAllowedDomains(Collections.emptyList()); |
||||||
|
jsonRpcConfiguration.setRpcApis(expectedApis); |
||||||
|
jsonRpcConfiguration.setMaxActiveConnections(1000); |
||||||
|
|
||||||
|
final GraphQLConfiguration graphQLConfiguration = GraphQLConfiguration.createDefault(); |
||||||
|
graphQLConfiguration.setEnabled(false); |
||||||
|
graphQLConfiguration.setHost("6.7.8.9"); |
||||||
|
graphQLConfiguration.setPort(6789); |
||||||
|
|
||||||
|
final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); |
||||||
|
webSocketConfiguration.setEnabled(false); |
||||||
|
webSocketConfiguration.setHost("9.10.11.12"); |
||||||
|
webSocketConfiguration.setPort(9101); |
||||||
|
webSocketConfiguration.setRpcApis(expectedApis); |
||||||
|
|
||||||
|
final MetricsConfiguration metricsConfiguration = |
||||||
|
MetricsConfiguration.builder().enabled(false).host("8.6.7.5").port(309).build(); |
||||||
|
|
||||||
|
parseCommand("--config-file", toml.toString()); |
||||||
|
|
||||||
|
verify(mockRunnerBuilder).discovery(eq(false)); |
||||||
|
verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkConfigArgumentCaptor.capture()); |
||||||
|
verify(mockRunnerBuilder).p2pAdvertisedHost(eq("1.2.3.4")); |
||||||
|
verify(mockRunnerBuilder).p2pListenPort(eq(1234)); |
||||||
|
verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); |
||||||
|
verify(mockRunnerBuilder).graphQLConfiguration(eq(graphQLConfiguration)); |
||||||
|
verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); |
||||||
|
verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration)); |
||||||
|
verify(mockRunnerBuilder).build(); |
||||||
|
|
||||||
|
final List<EnodeURL> nodes = |
||||||
|
asList( |
||||||
|
EnodeURLImpl.fromString("enode://" + VALID_NODE_ID + "@192.168.0.1:4567"), |
||||||
|
EnodeURLImpl.fromString("enode://" + VALID_NODE_ID + "@192.168.0.1:4567"), |
||||||
|
EnodeURLImpl.fromString("enode://" + VALID_NODE_ID + "@192.168.0.1:4567")); |
||||||
|
assertThat(ethNetworkConfigArgumentCaptor.getValue().getBootNodes()).isEqualTo(nodes); |
||||||
|
|
||||||
|
final EthNetworkConfig networkConfig = |
||||||
|
new EthNetworkConfig.Builder(EthNetworkConfig.getNetworkConfig(MAINNET)) |
||||||
|
.setNetworkId(BigInteger.valueOf(42)) |
||||||
|
.setGenesisConfig(encodeJsonGenesis(GENESIS_VALID_JSON)) |
||||||
|
.setBootNodes(nodes) |
||||||
|
.setDnsDiscoveryUrl(null) |
||||||
|
.build(); |
||||||
|
verify(mockControllerBuilder).dataDirectory(eq(dataFolder.toPath())); |
||||||
|
verify(mockControllerBuilderFactory).fromEthNetworkConfig(eq(networkConfig), any(), any()); |
||||||
|
verify(mockControllerBuilder).synchronizerConfiguration(syncConfigurationCaptor.capture()); |
||||||
|
|
||||||
|
assertThat(syncConfigurationCaptor.getValue().getSyncMode()).isEqualTo(SyncMode.FAST); |
||||||
|
assertThat(syncConfigurationCaptor.getValue().getFastSyncMinimumPeerCount()).isEqualTo(13); |
||||||
|
|
||||||
|
assertThat(commandOutput.toString(UTF_8)).isEmpty(); |
||||||
|
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the default values are not overridden if the key is not present in the configuration |
||||||
|
* file. The test checks if the default values for various settings remain unchanged when the |
||||||
|
* corresponding keys are not present in the configuration file. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() { |
||||||
|
final String configFile = this.getClass().getResource("/partial_config.toml").getFile(); |
||||||
|
|
||||||
|
parseCommand("--config-file", configFile); |
||||||
|
final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); |
||||||
|
|
||||||
|
final GraphQLConfiguration graphQLConfiguration = GraphQLConfiguration.createDefault(); |
||||||
|
|
||||||
|
final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); |
||||||
|
|
||||||
|
final MetricsConfiguration metricsConfiguration = MetricsConfiguration.builder().build(); |
||||||
|
|
||||||
|
verify(mockRunnerBuilder).discovery(eq(true)); |
||||||
|
verify(mockRunnerBuilder) |
||||||
|
.ethNetworkConfig( |
||||||
|
new EthNetworkConfig( |
||||||
|
EthNetworkConfig.jsonConfig(MAINNET), |
||||||
|
MAINNET.getNetworkId(), |
||||||
|
MAINNET_BOOTSTRAP_NODES, |
||||||
|
MAINNET_DISCOVERY_URL)); |
||||||
|
verify(mockRunnerBuilder).p2pAdvertisedHost(eq("127.0.0.1")); |
||||||
|
verify(mockRunnerBuilder).p2pListenPort(eq(30303)); |
||||||
|
verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); |
||||||
|
verify(mockRunnerBuilder).graphQLConfiguration(eq(graphQLConfiguration)); |
||||||
|
verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); |
||||||
|
verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration)); |
||||||
|
verify(mockRunnerBuilder).build(); |
||||||
|
verify(mockControllerBuilder).build(); |
||||||
|
verify(mockControllerBuilder).synchronizerConfiguration(syncConfigurationCaptor.capture()); |
||||||
|
|
||||||
|
final SynchronizerConfiguration syncConfig = syncConfigurationCaptor.getValue(); |
||||||
|
assertThat(syncConfig.getSyncMode()).isEqualTo(SyncMode.FAST); |
||||||
|
assertThat(syncConfig.getFastSyncMinimumPeerCount()).isEqualTo(5); |
||||||
|
|
||||||
|
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); |
||||||
|
|
||||||
|
assertThat(commandOutput.toString(UTF_8)).isEmpty(); |
||||||
|
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the environment variable overrides the value from the configuration file. The test |
||||||
|
* checks if the value of the miner's coinbase address set through an environment variable |
||||||
|
* correctly overrides the value specified in the configuration file. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void envVariableOverridesValueFromConfigFile() { |
||||||
|
final String configFile = this.getClass().getResource("/partial_config.toml").getFile(); |
||||||
|
final String expectedCoinbase = "0x0000000000000000000000000000000000000004"; |
||||||
|
setEnvironmentVariable("BESU_MINER_COINBASE", expectedCoinbase); |
||||||
|
parseCommand("--config-file", configFile); |
||||||
|
|
||||||
|
verify(mockControllerBuilder) |
||||||
|
.miningParameters( |
||||||
|
ImmutableMiningParameters.builder() |
||||||
|
.mutableInitValues( |
||||||
|
ImmutableMiningParameters.MutableInitValues.builder() |
||||||
|
.coinbase(Address.fromHexString(expectedCoinbase)) |
||||||
|
.build()) |
||||||
|
.build()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the command line option overrides the environment variable and configuration. The test |
||||||
|
* checks if the value of the miner's coinbase address set through a command line option correctly |
||||||
|
* overrides the value specified in the environment variable and the configuration file. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void cliOptionOverridesEnvVariableAndConfig() { |
||||||
|
final String configFile = this.getClass().getResource("/partial_config.toml").getFile(); |
||||||
|
final String expectedCoinbase = "0x0000000000000000000000000000000000000006"; |
||||||
|
setEnvironmentVariable("BESU_MINER_COINBASE", "0x0000000000000000000000000000000000000004"); |
||||||
|
parseCommand("--config-file", configFile, "--miner-coinbase", expectedCoinbase); |
||||||
|
|
||||||
|
verify(mockControllerBuilder) |
||||||
|
.miningParameters( |
||||||
|
ImmutableMiningParameters.builder() |
||||||
|
.mutableInitValues( |
||||||
|
ImmutableMiningParameters.MutableInitValues.builder() |
||||||
|
.coinbase(Address.fromHexString(expectedCoinbase)) |
||||||
|
.build()) |
||||||
|
.build()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the profile option sets the correct defaults. The test checks if the 'dev' profile |
||||||
|
* correctly sets the network ID to the expected value. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void profileOptionShouldSetCorrectDefaults() { |
||||||
|
final ArgumentCaptor<EthNetworkConfig> networkArg = |
||||||
|
ArgumentCaptor.forClass(EthNetworkConfig.class); |
||||||
|
|
||||||
|
parseCommand("--profile", "dev"); |
||||||
|
verify(mockControllerBuilderFactory).fromEthNetworkConfig(networkArg.capture(), any(), any()); |
||||||
|
verify(mockControllerBuilder).build(); |
||||||
|
|
||||||
|
final EthNetworkConfig config = networkArg.getValue(); |
||||||
|
assertThat(config.getNetworkId()).isEqualTo(DEV.getNetworkId()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the command line option overrides the profile configuration. The test checks if the |
||||||
|
* network ID set through a command line option correctly overrides the value specified in the |
||||||
|
* 'dev' profile. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void cliOptionOverridesProfileConfiguration() { |
||||||
|
final ArgumentCaptor<EthNetworkConfig> networkArg = |
||||||
|
ArgumentCaptor.forClass(EthNetworkConfig.class); |
||||||
|
|
||||||
|
parseCommand("--profile", "dev", "--network", "MAINNET"); |
||||||
|
verify(mockControllerBuilderFactory).fromEthNetworkConfig(networkArg.capture(), any(), any()); |
||||||
|
verify(mockControllerBuilder).build(); |
||||||
|
|
||||||
|
final EthNetworkConfig config = networkArg.getValue(); |
||||||
|
assertThat(config.getNetworkId()).isEqualTo(MAINNET.getNetworkId()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the configuration file overrides the profile configuration. The test checks if the |
||||||
|
* network ID specified in the configuration file correctly overrides the value specified in the |
||||||
|
* 'dev' profile. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void configFileOverridesProfileConfiguration() { |
||||||
|
final ArgumentCaptor<EthNetworkConfig> networkArg = |
||||||
|
ArgumentCaptor.forClass(EthNetworkConfig.class); |
||||||
|
|
||||||
|
final String configFile = this.getClass().getResource("/partial_config.toml").getFile(); |
||||||
|
parseCommand("--profile", "dev", "--config-file", configFile); |
||||||
|
verify(mockControllerBuilderFactory).fromEthNetworkConfig(networkArg.capture(), any(), any()); |
||||||
|
verify(mockControllerBuilder).build(); |
||||||
|
|
||||||
|
final EthNetworkConfig config = networkArg.getValue(); |
||||||
|
assertThat(config.getNetworkId()).isEqualTo(MAINNET.getNetworkId()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test if the environment variable overrides the profile configuration. The test checks if the |
||||||
|
* network ID set through an environment variable correctly overrides the value specified in the |
||||||
|
* 'dev' profile. |
||||||
|
*/ |
||||||
|
@Test |
||||||
|
public void environmentVariableOverridesProfileConfiguration() { |
||||||
|
final ArgumentCaptor<EthNetworkConfig> networkArg = |
||||||
|
ArgumentCaptor.forClass(EthNetworkConfig.class); |
||||||
|
setEnvironmentVariable("BESU_NETWORK", "MAINNET"); |
||||||
|
parseCommand("--profile", "dev"); |
||||||
|
verify(mockControllerBuilderFactory).fromEthNetworkConfig(networkArg.capture(), any(), any()); |
||||||
|
verify(mockControllerBuilder).build(); |
||||||
|
|
||||||
|
final EthNetworkConfig config = networkArg.getValue(); |
||||||
|
assertThat(config.getNetworkId()).isEqualTo(MAINNET.getNetworkId()); |
||||||
|
} |
||||||
|
} |
@ -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"); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
network="DEV" |
@ -0,0 +1,79 @@ |
|||||||
|
/* |
||||||
|
* 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.ethereum.api.jsonrpc.internal.methods; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.FilterParameter; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.processor.BlockTracer; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; |
||||||
|
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; |
||||||
|
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; |
||||||
|
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; |
||||||
|
|
||||||
|
import java.util.function.Supplier; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||||
|
import org.junit.jupiter.params.ParameterizedTest; |
||||||
|
import org.junit.jupiter.params.provider.CsvSource; |
||||||
|
import org.mockito.Mock; |
||||||
|
import org.mockito.junit.jupiter.MockitoExtension; |
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class) |
||||||
|
public class TraceFilterTest { |
||||||
|
|
||||||
|
private TraceFilter method; |
||||||
|
|
||||||
|
@Mock Supplier<BlockTracer> blockTracerSupplier; |
||||||
|
@Mock ProtocolSchedule protocolSchedule; |
||||||
|
@Mock BlockchainQueries blockchainQueries; |
||||||
|
|
||||||
|
@ParameterizedTest |
||||||
|
@CsvSource({ |
||||||
|
"0, 1001, 1000", "0, 5000, 1000", "1, 1002, 1000", "1, 6002, 1000", "1000, 3000, 1000", |
||||||
|
"0, 501, 500", "0, 5000, 500", "1, 502, 500", "1, 6002, 500", "1000, 3000, 500" |
||||||
|
}) |
||||||
|
public void shouldFailIfParamsExceedMaxRange( |
||||||
|
final long fromBlock, final long toBlock, final long maxFilterRange) { |
||||||
|
final FilterParameter filterParameter = |
||||||
|
new FilterParameter( |
||||||
|
new BlockParameter(fromBlock), |
||||||
|
new BlockParameter(toBlock), |
||||||
|
null, |
||||||
|
null, |
||||||
|
null, |
||||||
|
null, |
||||||
|
null, |
||||||
|
null, |
||||||
|
null); |
||||||
|
|
||||||
|
JsonRpcRequestContext request = |
||||||
|
new JsonRpcRequestContext( |
||||||
|
new JsonRpcRequest("2.0", "trace_filter", new Object[] {filterParameter})); |
||||||
|
|
||||||
|
method = |
||||||
|
new TraceFilter(blockTracerSupplier, protocolSchedule, blockchainQueries, maxFilterRange); |
||||||
|
|
||||||
|
final JsonRpcResponse response = method.response(request); |
||||||
|
assertThat(response).isInstanceOf(JsonRpcErrorResponse.class); |
||||||
|
|
||||||
|
final JsonRpcErrorResponse errorResponse = (JsonRpcErrorResponse) response; |
||||||
|
assertThat(errorResponse.getErrorType()).isEqualTo(RpcErrorType.EXCEEDS_RPC_MAX_BLOCK_RANGE); |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue