diff --git a/CHANGELOG.md b/CHANGELOG.md index 02c839efdd..ba9a4e7a2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ## 24.1.2-SNAPSHOT ### Breaking Changes -- The `trace-filter` method in JSON-RPC API now has a default block range limit of 1000, adjustable with `--rpc-max-trace-filter-range` [#6446](https://github.com/hyperledger/besu/pull/6446) +- The `trace-filter` method in JSON-RPC API now has a default block range limit of 1000, adjustable with `--rpc-max-trace-filter-range` (thanks @alyokaz) [#6446](https://github.com/hyperledger/besu/pull/6446) +- Requesting the Ethereum Node Record (ENR) to acquire the fork id from bonded peers is now enabled by default, so the following change has been made [#5628](https://github.com/hyperledger/besu/pull/5628): + - `--Xfilter-on-enr-fork-id` has been removed. To disable the feature use `--filter-on-enr-fork-id=false`. ### Deprecations @@ -11,6 +13,8 @@ - Add `OperationTracer.tracePrepareTransaction`, where the sender account has not yet been altered[#6453](https://github.com/hyperledger/besu/pull/6453) - Improve the high spec flag by limiting it to a few column families [#6354](https://github.com/hyperledger/besu/pull/6354) - Log blob count when importing a block via Engine API [#6466](https://github.com/hyperledger/besu/pull/6466) +- Introduce `--Xbonsai-limit-trie-logs-enabled` experimental feature which by default will only retain the latest 512 trie logs, saving about 3GB per week in database growth [#5390](https://github.com/hyperledger/besu/issues/5390) +- Introduce `besu storage x-trie-log prune` experimental offline subcommand which will prune all redundant trie logs except the latest 512 [#6303](https://github.com/hyperledger/besu/pull/6303) ### Bug fixes - Fix the way an advertised host configured with `--p2p-host` is treated when communicating with the originator of a PING packet [#6225](https://github.com/hyperledger/besu/pull/6225) @@ -22,11 +26,9 @@ ### Breaking Changes - New `EXECUTION_HALTED` error returned if there is an error executing or simulating a transaction, with the reason for execution being halted. Replaces the generic `INTERNAL_ERROR` return code in certain cases which some applications may be checking for [#6343](https://github.com/hyperledger/besu/pull/6343) -- The Besu Docker images with `openjdk-latest` tags since 23.10.3 were incorrectly using UID 1001 instead of 1000 for the container's `besu` user. The user now uses 1000 again. Containers created from or migrated to images using UID 1001 will need to chown their persistent database files to UID 1000 [#6360](https://github.com/hyperledger/besu/pull/6360) +- The Besu Docker images with `openjdk-latest` tags since 23.10.3 were incorrectly using UID 1001 instead of 1000 for the container's `besu` user. The user now uses 1000 again. Containers created from or migrated to images using UID 1001 will need to chown their persistent database files to UID 1000 (thanks @h4l) [#6360](https://github.com/hyperledger/besu/pull/6360) - The deprecated `--privacy-onchain-groups-enabled` option has now been removed. Use the `--privacy-flexible-groups-enabled` option instead. [#6411](https://github.com/hyperledger/besu/pull/6411) -- Requesting the Ethereum Node Record (ENR) to acquire the fork id from bonded peers is now enabled by default, so the following change has been made [#5628](https://github.com/hyperledger/besu/pull/5628): - - `--Xfilter-on-enr-fork-id` has been removed. To disable the feature use `--filter-on-enr-fork-id=false`. -- The time that can be spent selecting transactions during block creation is not capped at 5 seconds for PoS and PoW networks, and for PoA networks, at 75% of the block period specified in the genesis, this to prevent possible DoS in case a single transaction is taking too long to execute, and to have a stable block production rate, but it could be a breaking change if an existing network used to have transactions that takes more time to executed that the newly introduced limit, if it is mandatory for these network to keep processing these long processing transaction, then the default value of `block-txs-selection-max-time` or `poa-block-txs-selection-max-time` needs to be tuned accordingly. +- The time that can be spent selecting transactions during block creation is not capped at 5 seconds for PoS and PoW networks, and for PoA networks, at 75% of the block period specified in the genesis, this to prevent possible DoS in case a single transaction is taking too long to execute, and to have a stable block production rate, but it could be a breaking change if an existing network used to have transactions that takes more time to executed that the newly introduced limit, if it is mandatory for these network to keep processing these long processing transaction, then the default value of `block-txs-selection-max-time` or `poa-block-txs-selection-max-time` needs to be tuned accordingly. [#6423](https://github.com/hyperledger/besu/pull/6423) ### Deprecations @@ -40,8 +42,6 @@ - Upgrade Mockito [#6397](https://github.com/hyperledger/besu/pull/6397) - Upgrade `tech.pegasys.discovery:discovery` [#6414](https://github.com/hyperledger/besu/pull/6414) - Options to tune the max allowed time that can be spent selecting transactions during block creation are now stable [#6423](https://github.com/hyperledger/besu/pull/6423) -- Introduce `--Xbonsai-limit-trie-logs-enabled` experimental feature which by default will only retain the latest 512 trie logs, saving about 3GB per week in database growth [#5390](https://github.com/hyperledger/besu/issues/5390) -- Introduce `besu storage x-trie-log prune` experimental offline subcommand which will prune all redundant trie logs except the latest 512 [#6303](https://github.com/hyperledger/besu/pull/6303) ### Bug fixes - INTERNAL_ERROR from `eth_estimateGas` JSON/RPC calls [#6344](https://github.com/hyperledger/besu/issues/6344) @@ -49,8 +49,17 @@ - Fluent EVM API definition for Tangerine Whistle had incorrect code size validation configured [#6382](https://github.com/hyperledger/besu/pull/6382) - Correct mining beneficiary for Clique networks in TraceServiceImpl [#6390](https://github.com/hyperledger/besu/pull/6390) - Fix to gas limit delta calculations used in block production. Besu should now increment or decrement the block gas limit towards its target correctly (thanks @arbora) #6425 +- Ensure Backward Sync waits for initial sync before starting a session [#6455](https://github.com/hyperledger/besu/issues/6455) +- Silence the noisy DNS query errors [#6458](https://github.com/hyperledger/besu/issues/6458) ### Download Links +https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.1/besu-24.1.1.zip / sha256 e23c5b790180756964a70dcdd575ee2ed2c2efa79af00bce956d23bd2f7dc67c +https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.1/besu-24.1.1.tar.gz / sha256 4b0ddd5a25be2df5d2324bff935785eb63e4e3a5f421614ea690bacb5b9cb344 + +### Errata +Note, due to a CI race with the release job, the initial published version of 24.1.1 were overwritten by artifacts generated from the same sources, but differ in their embedded timestamps. The initial SHAs are noted here but are deprecated: +~~https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.1/besu-24.1.1.zip / sha256 b6b64f939e0bb4937ce90fc647e0a7073ce3e359c10352b502059955070a60c6 +https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.1/besu-24.1.1.tar.gz / sha256 cfcae04c30769bf338b0740ac65870f9346d3469931bb46cdba3b2f65d311e7a~~ ## 24.1.0 @@ -73,8 +82,8 @@ - mitigation for trielog failure [#6315]((https://github.com/hyperledger/besu/pull/6315) ### Download Links -https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.0/besu-24.1.0.zip / sha256 TBA -https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.0/besu-24.1.0.tar.gz / sha256 TBA +https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.0/besu-24.1.0.zip / sha256 d36c8aeef70f0a516d4c26d3bc696c3e2a671e515c9e6e9475a31fe759e39f64 +https://hyperledger.jfrog.io/artifactory/besu-binaries/besu/24.1.0/besu-24.1.0.tar.gz / sha256 602b04c0729a7b17361d1f0b39f4ce6a2ebe47932165add666560fe594d9ca99 ## 23.10.3-hotfix diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index b9a137cbc1..68c3a6a5ab 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -26,10 +26,6 @@ import static org.hyperledger.besu.cli.util.CommandLineUtils.isOptionSet; import static org.hyperledger.besu.controller.BesuController.DATABASE_PATH; import static org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration.DEFAULT_GRAPHQL_HTTP_PORT; import static org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration.DEFAULT_ENGINE_JSON_RPC_PORT; -import static org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration.DEFAULT_JSON_RPC_PORT; -import static org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration.DEFAULT_PRETTY_JSON_ENABLED; -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.metrics.BesuMetricCategory.DEFAULT_METRIC_CATEGORIES; import static org.hyperledger.besu.metrics.MetricsProtocol.PROMETHEUS; @@ -50,13 +46,13 @@ import org.hyperledger.besu.cli.converter.MetricCategoryConverter; import org.hyperledger.besu.cli.converter.PercentageConverter; import org.hyperledger.besu.cli.custom.CorsAllowedOriginsProperty; import org.hyperledger.besu.cli.custom.JsonRPCAllowlistHostsProperty; -import org.hyperledger.besu.cli.custom.RpcAuthFileValidator; import org.hyperledger.besu.cli.error.BesuExecutionExceptionHandler; import org.hyperledger.besu.cli.error.BesuParameterExceptionHandler; import org.hyperledger.besu.cli.options.MiningOptions; import org.hyperledger.besu.cli.options.TransactionPoolOptions; import org.hyperledger.besu.cli.options.stable.DataStorageOptions; import org.hyperledger.besu.cli.options.stable.EthstatsOptions; +import org.hyperledger.besu.cli.options.stable.JsonRpcHttpOptions; import org.hyperledger.besu.cli.options.stable.LoggingLevelOption; import org.hyperledger.besu.cli.options.stable.NodePrivateKeyFileOption; import org.hyperledger.besu.cli.options.stable.P2PTLSConfigOptions; @@ -115,14 +111,10 @@ import org.hyperledger.besu.ethereum.api.ImmutableApiConfiguration; import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; -import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm; import org.hyperledger.besu.ethereum.api.jsonrpc.ipc.JsonRpcIpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; -import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider; -import org.hyperledger.besu.ethereum.api.tls.TlsClientAuthConfiguration; -import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.MiningParameters; import org.hyperledger.besu.ethereum.core.PrivacyParameters; @@ -212,8 +204,6 @@ import java.net.SocketException; import java.net.URI; import java.net.UnknownHostException; import java.nio.file.Path; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; @@ -231,8 +221,6 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; @@ -647,162 +635,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { // JSON-RPC HTTP Options @CommandLine.ArgGroup(validate = false, heading = "@|bold JSON-RPC HTTP Options|@%n") - JsonRPCHttpOptionGroup jsonRPCHttpOptionGroup = new JsonRPCHttpOptionGroup(); - - static class JsonRPCHttpOptionGroup { - @Option( - names = {"--rpc-http-enabled"}, - description = "Set to start the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") - private final Boolean isRpcHttpEnabled = false; - - @SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings. - @Option( - names = {"--rpc-http-host"}, - paramLabel = MANDATORY_HOST_FORMAT_HELP, - description = "Host for JSON-RPC HTTP to listen on (default: ${DEFAULT-VALUE})", - arity = "1") - private String rpcHttpHost; - - @Option( - names = {"--rpc-http-port"}, - paramLabel = MANDATORY_PORT_FORMAT_HELP, - description = "Port for JSON-RPC HTTP to listen on (default: ${DEFAULT-VALUE})", - arity = "1") - private final Integer rpcHttpPort = DEFAULT_JSON_RPC_PORT; - - @Option( - names = {"--rpc-http-max-active-connections"}, - description = - "Maximum number of HTTP connections allowed for JSON-RPC (default: ${DEFAULT-VALUE}). Once this limit is reached, incoming connections will be rejected.", - arity = "1") - private final Integer rpcHttpMaxConnections = DEFAULT_HTTP_MAX_CONNECTIONS; - - // A list of origins URLs that are accepted by the JsonRpcHttpServer (CORS) - @Option( - names = {"--rpc-http-cors-origins"}, - description = "Comma separated origin domain URLs for CORS validation (default: none)") - private final CorsAllowedOriginsProperty rpcHttpCorsAllowedOrigins = - new CorsAllowedOriginsProperty(); - - @Option( - names = {"--rpc-http-api", "--rpc-http-apis"}, - paramLabel = "", - split = " {0,1}, {0,1}", - arity = "1..*", - description = - "Comma separated list of APIs to enable on JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") - private final List rpcHttpApis = DEFAULT_RPC_APIS; - - @Option( - names = {"--rpc-http-api-method-no-auth", "--rpc-http-api-methods-no-auth"}, - paramLabel = "", - split = " {0,1}, {0,1}", - arity = "1..*", - description = - "Comma separated list of API methods to exclude from RPC authentication services, RPC HTTP authentication must be enabled") - private final List rpcHttpApiMethodsNoAuth = new ArrayList(); - - @Option( - names = {"--rpc-http-authentication-enabled"}, - description = - "Require authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") - private final Boolean isRpcHttpAuthenticationEnabled = false; - - @SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings. - @CommandLine.Option( - names = {"--rpc-http-authentication-credentials-file"}, - paramLabel = MANDATORY_FILE_FORMAT_HELP, - description = - "Storage file for JSON-RPC HTTP authentication credentials (default: ${DEFAULT-VALUE})", - arity = "1") - private String rpcHttpAuthenticationCredentialsFile = null; - - @CommandLine.Option( - names = {"--rpc-http-authentication-jwt-public-key-file"}, - paramLabel = MANDATORY_FILE_FORMAT_HELP, - description = "JWT public key file for JSON-RPC HTTP authentication", - arity = "1") - private final File rpcHttpAuthenticationPublicKeyFile = null; - - @Option( - names = {"--rpc-http-authentication-jwt-algorithm"}, - description = - "Encryption algorithm used for HTTP JWT public key. Possible values are ${COMPLETION-CANDIDATES}" - + " (default: ${DEFAULT-VALUE})", - arity = "1") - private final JwtAlgorithm rpcHttpAuthenticationAlgorithm = DEFAULT_JWT_ALGORITHM; - - @Option( - names = {"--rpc-http-tls-enabled"}, - description = "Enable TLS for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") - private final Boolean isRpcHttpTlsEnabled = false; - - @Option( - names = {"--rpc-http-tls-keystore-file"}, - paramLabel = MANDATORY_FILE_FORMAT_HELP, - description = - "Keystore (PKCS#12) containing key/certificate for the JSON-RPC HTTP service. Required if TLS is enabled.") - private final Path rpcHttpTlsKeyStoreFile = null; - - @Option( - names = {"--rpc-http-tls-keystore-password-file"}, - paramLabel = MANDATORY_FILE_FORMAT_HELP, - description = - "File containing password to unlock keystore for the JSON-RPC HTTP service. Required if TLS is enabled.") - private final Path rpcHttpTlsKeyStorePasswordFile = null; - - @Option( - names = {"--rpc-http-tls-client-auth-enabled"}, - description = - "Enable TLS client authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") - private final Boolean isRpcHttpTlsClientAuthEnabled = false; - - @Option( - names = {"--rpc-http-tls-known-clients-file"}, - paramLabel = MANDATORY_FILE_FORMAT_HELP, - description = - "Path to file containing clients certificate common name and fingerprint for client authentication") - private final Path rpcHttpTlsKnownClientsFile = null; - - @Option( - names = {"--rpc-http-tls-ca-clients-enabled"}, - description = - "Enable to accept clients certificate signed by a valid CA for client authentication (default: ${DEFAULT-VALUE})") - private final Boolean isRpcHttpTlsCAClientsEnabled = false; - - @Option( - names = {"--rpc-http-tls-protocol", "--rpc-http-tls-protocols"}, - description = - "Comma separated list of TLS protocols to support (default: ${DEFAULT-VALUE})", - split = ",", - arity = "1..*") - private final List rpcHttpTlsProtocols = new ArrayList<>(DEFAULT_TLS_PROTOCOLS); - - @Option( - names = {"--rpc-http-tls-cipher-suite", "--rpc-http-tls-cipher-suites"}, - description = "Comma separated list of TLS cipher suites to support", - split = ",", - arity = "1..*") - private final List rpcHttpTlsCipherSuites = new ArrayList<>(); - - @CommandLine.Option( - names = {"--rpc-http-max-batch-size"}, - paramLabel = MANDATORY_INTEGER_FORMAT_HELP, - description = - "Specifies the maximum number of requests in a single RPC batch request via RPC. -1 specifies no limit (default: ${DEFAULT-VALUE})") - private final Integer rpcHttpMaxBatchSize = DEFAULT_HTTP_MAX_BATCH_SIZE; - - @CommandLine.Option( - names = {"--rpc-http-max-request-content-length"}, - paramLabel = MANDATORY_LONG_FORMAT_HELP, - description = "Specifies the maximum request content length. (default: ${DEFAULT-VALUE})") - private final Long rpcHttpMaxRequestContentLength = DEFAULT_MAX_REQUEST_CONTENT_LENGTH; - - @Option( - names = {"--json-pretty-print-enabled"}, - description = "Enable JSON pretty print format (default: ${DEFAULT-VALUE})") - private final Boolean prettyJsonEnabled = DEFAULT_PRETTY_JSON_ENABLED; - } + JsonRpcHttpOptions jsonRpcHttpOptions = new JsonRpcHttpOptions(); // JSON-RPC Websocket Options @CommandLine.ArgGroup(validate = false, heading = "@|bold JSON-RPC Websocket Options|@%n") @@ -1866,26 +1699,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { Arrays.stream(RpcApis.values()) .anyMatch(builtInApi -> apiName.equals(builtInApi.name())) || rpcEndpointServiceImpl.hasNamespace(apiName); - - if (!jsonRPCHttpOptionGroup.rpcHttpApis.stream().allMatch(configuredApis)) { - final List invalidHttpApis = - new ArrayList(jsonRPCHttpOptionGroup.rpcHttpApis); - invalidHttpApis.removeAll(VALID_APIS); - throw new ParameterException( - this.commandLine, - "Invalid value for option '--rpc-http-api': invalid entries found " - + invalidHttpApis.toString()); - } - - final boolean validHttpApiMethods = - jsonRPCHttpOptionGroup.rpcHttpApiMethodsNoAuth.stream() - .allMatch(RpcMethod::rpcMethodExists); - - if (!validHttpApiMethods) { - throw new ParameterException( - this.commandLine, - "Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods"); - } + jsonRpcHttpOptions.validate(logger, commandLine, configuredApis); } private void validateRpcWsOptions() { @@ -1987,8 +1801,10 @@ public class BesuCommand implements DefaultCommandValues, Runnable { ethNetworkConfig = updateNetworkConfig(network); jsonRpcConfiguration = - jsonRpcConfiguration( - jsonRPCHttpOptionGroup.rpcHttpPort, jsonRPCHttpOptionGroup.rpcHttpApis, hostsAllowlist); + jsonRpcHttpOptions.jsonRpcConfiguration( + hostsAllowlist, + p2PDiscoveryOptionGroup.autoDiscoverDefaultIP().getHostAddress(), + unstableRPCOptions.getHttpTimeoutSec()); if (isEngineApiEnabled()) { engineJsonRpcConfiguration = createEngineJsonRpcConfiguration( @@ -2160,9 +1976,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable { } private JsonRpcConfiguration createEngineJsonRpcConfiguration( - final Integer listenPort, final List allowCallsFrom) { + final Integer engineListenPort, final List allowCallsFrom) { + jsonRpcHttpOptions.checkDependencies(logger, commandLine); final JsonRpcConfiguration engineConfig = - jsonRpcConfiguration(listenPort, Arrays.asList("ENGINE", "ETH"), allowCallsFrom); + jsonRpcHttpOptions.jsonRpcConfiguration( + allowCallsFrom, + p2PDiscoveryOptionGroup.autoDiscoverDefaultIP().getHostAddress(), + unstableRPCOptions.getWsTimeoutSec()); + engineConfig.setPort(engineListenPort); + engineConfig.setRpcApis(Arrays.asList("ENGINE", "ETH")); engineConfig.setEnabled(isEngineApiEnabled()); if (!engineRPCOptionGroup.isEngineAuthDisabled) { engineConfig.setAuthenticationEnabled(true); @@ -2178,116 +2000,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable { return engineConfig; } - private JsonRpcConfiguration jsonRpcConfiguration( - final Integer listenPort, final List apiGroups, final List allowCallsFrom) { - checkRpcTlsClientAuthOptionsDependencies(); - checkRpcTlsOptionsDependencies(); - checkRpcHttpOptionsDependencies(); - - if (jsonRPCHttpOptionGroup.isRpcHttpAuthenticationEnabled) { - CommandLineUtils.checkOptionDependencies( - logger, - commandLine, - "--rpc-http-authentication-public-key-file", - jsonRPCHttpOptionGroup.rpcHttpAuthenticationPublicKeyFile == null, - asList("--rpc-http-authentication-jwt-algorithm")); - } - - if (jsonRPCHttpOptionGroup.isRpcHttpAuthenticationEnabled - && rpcHttpAuthenticationCredentialsFile() == null - && jsonRPCHttpOptionGroup.rpcHttpAuthenticationPublicKeyFile == null) { - throw new ParameterException( - commandLine, - "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file"); - } - - final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); - jsonRpcConfiguration.setEnabled(jsonRPCHttpOptionGroup.isRpcHttpEnabled); - jsonRpcConfiguration.setHost( - Strings.isNullOrEmpty(jsonRPCHttpOptionGroup.rpcHttpHost) - ? p2PDiscoveryOptionGroup.autoDiscoverDefaultIP().getHostAddress() - : jsonRPCHttpOptionGroup.rpcHttpHost); - jsonRpcConfiguration.setPort(listenPort); - jsonRpcConfiguration.setMaxActiveConnections(jsonRPCHttpOptionGroup.rpcHttpMaxConnections); - jsonRpcConfiguration.setCorsAllowedDomains(jsonRPCHttpOptionGroup.rpcHttpCorsAllowedOrigins); - jsonRpcConfiguration.setRpcApis(apiGroups.stream().distinct().collect(Collectors.toList())); - jsonRpcConfiguration.setNoAuthRpcApis( - jsonRPCHttpOptionGroup.rpcHttpApiMethodsNoAuth.stream() - .distinct() - .collect(Collectors.toList())); - jsonRpcConfiguration.setHostsAllowlist(allowCallsFrom); - jsonRpcConfiguration.setAuthenticationEnabled( - jsonRPCHttpOptionGroup.isRpcHttpAuthenticationEnabled); - jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile()); - jsonRpcConfiguration.setAuthenticationPublicKeyFile( - jsonRPCHttpOptionGroup.rpcHttpAuthenticationPublicKeyFile); - jsonRpcConfiguration.setAuthenticationAlgorithm( - jsonRPCHttpOptionGroup.rpcHttpAuthenticationAlgorithm); - jsonRpcConfiguration.setTlsConfiguration(rpcHttpTlsConfiguration()); - jsonRpcConfiguration.setHttpTimeoutSec(unstableRPCOptions.getHttpTimeoutSec()); - jsonRpcConfiguration.setMaxBatchSize(jsonRPCHttpOptionGroup.rpcHttpMaxBatchSize); - jsonRpcConfiguration.setMaxRequestContentLength( - jsonRPCHttpOptionGroup.rpcHttpMaxRequestContentLength); - jsonRpcConfiguration.setPrettyJsonEnabled(jsonRPCHttpOptionGroup.prettyJsonEnabled); - return jsonRpcConfiguration; - } - - private void checkRpcHttpOptionsDependencies() { - CommandLineUtils.checkOptionDependencies( - logger, - commandLine, - "--rpc-http-enabled", - !jsonRPCHttpOptionGroup.isRpcHttpEnabled, - asList( - "--rpc-http-api", - "--rpc-http-apis", - "--rpc-http-api-method-no-auth", - "--rpc-http-api-methods-no-auth", - "--rpc-http-cors-origins", - "--rpc-http-host", - "--rpc-http-port", - "--rpc-http-max-active-connections", - "--rpc-http-authentication-enabled", - "--rpc-http-authentication-credentials-file", - "--rpc-http-authentication-public-key-file", - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - "--rpc-http-tls-keystore-password-file", - "--rpc-http-tls-client-auth-enabled", - "--rpc-http-tls-known-clients-file", - "--rpc-http-tls-ca-clients-enabled", - "--rpc-http-authentication-jwt-algorithm", - "--rpc-http-tls-protocols", - "--rpc-http-tls-cipher-suite", - "--rpc-http-tls-cipher-suites")); - } - - private void checkRpcTlsOptionsDependencies() { - CommandLineUtils.checkOptionDependencies( - logger, - commandLine, - "--rpc-http-tls-enabled", - !jsonRPCHttpOptionGroup.isRpcHttpTlsEnabled, - asList( - "--rpc-http-tls-keystore-file", - "--rpc-http-tls-keystore-password-file", - "--rpc-http-tls-client-auth-enabled", - "--rpc-http-tls-known-clients-file", - "--rpc-http-tls-ca-clients-enabled", - "--rpc-http-tls-protocols", - "--rpc-http-tls-cipher-suite", - "--rpc-http-tls-cipher-suites")); - } - - private void checkRpcTlsClientAuthOptionsDependencies() { - CommandLineUtils.checkOptionDependencies( - logger, - commandLine, - "--rpc-http-tls-client-auth-enabled", - !jsonRPCHttpOptionGroup.isRpcHttpTlsClientAuthEnabled, - asList("--rpc-http-tls-known-clients-file", "--rpc-http-tls-ca-clients-enabled")); - } - private void checkPrivacyTlsOptionsDependencies() { CommandLineUtils.checkOptionDependencies( logger, @@ -2300,75 +2012,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable { "--privacy-tls-known-enclave-file")); } - private Optional rpcHttpTlsConfiguration() { - if (!isRpcTlsConfigurationRequired()) { - return Optional.empty(); - } - - if (jsonRPCHttpOptionGroup.rpcHttpTlsKeyStoreFile == null) { - throw new ParameterException( - commandLine, "Keystore file is required when TLS is enabled for JSON-RPC HTTP endpoint"); - } - - if (jsonRPCHttpOptionGroup.rpcHttpTlsKeyStorePasswordFile == null) { - throw new ParameterException( - commandLine, - "File containing password to unlock keystore is required when TLS is enabled for JSON-RPC HTTP endpoint"); - } - - if (jsonRPCHttpOptionGroup.isRpcHttpTlsClientAuthEnabled - && !jsonRPCHttpOptionGroup.isRpcHttpTlsCAClientsEnabled - && jsonRPCHttpOptionGroup.rpcHttpTlsKnownClientsFile == null) { - throw new ParameterException( - commandLine, - "Known-clients file must be specified or CA clients must be enabled when TLS client authentication is enabled for JSON-RPC HTTP endpoint"); - } - - jsonRPCHttpOptionGroup.rpcHttpTlsProtocols.retainAll(getJDKEnabledProtocols()); - if (jsonRPCHttpOptionGroup.rpcHttpTlsProtocols.isEmpty()) { - throw new ParameterException( - commandLine, - "No valid TLS protocols specified (the following protocols are enabled: " - + getJDKEnabledProtocols() - + ")"); - } - - for (final String cipherSuite : jsonRPCHttpOptionGroup.rpcHttpTlsCipherSuites) { - if (!getJDKEnabledCipherSuites().contains(cipherSuite)) { - throw new ParameterException( - commandLine, "Invalid TLS cipher suite specified " + cipherSuite); - } - } - - jsonRPCHttpOptionGroup.rpcHttpTlsCipherSuites.retainAll(getJDKEnabledCipherSuites()); - - return Optional.of( - TlsConfiguration.Builder.aTlsConfiguration() - .withKeyStorePath(jsonRPCHttpOptionGroup.rpcHttpTlsKeyStoreFile) - .withKeyStorePasswordSupplier( - new FileBasedPasswordProvider( - jsonRPCHttpOptionGroup.rpcHttpTlsKeyStorePasswordFile)) - .withClientAuthConfiguration(rpcHttpTlsClientAuthConfiguration()) - .withSecureTransportProtocols(jsonRPCHttpOptionGroup.rpcHttpTlsProtocols) - .withCipherSuites(jsonRPCHttpOptionGroup.rpcHttpTlsCipherSuites) - .build()); - } - - private TlsClientAuthConfiguration rpcHttpTlsClientAuthConfiguration() { - if (jsonRPCHttpOptionGroup.isRpcHttpTlsClientAuthEnabled) { - return TlsClientAuthConfiguration.Builder.aTlsClientAuthConfiguration() - .withKnownClientsFile(jsonRPCHttpOptionGroup.rpcHttpTlsKnownClientsFile) - .withCaClientsEnabled(jsonRPCHttpOptionGroup.isRpcHttpTlsCAClientsEnabled) - .build(); - } - - return null; - } - - private boolean isRpcTlsConfigurationRequired() { - return jsonRPCHttpOptionGroup.isRpcHttpEnabled && jsonRPCHttpOptionGroup.isRpcHttpTlsEnabled; - } - private ApiConfiguration apiConfiguration() { checkApiOptionsDependencies(); var builder = @@ -2450,7 +2093,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { private Optional permissioningConfiguration() throws Exception { if (!(localPermissionsEnabled() || contractPermissionsEnabled())) { - if (jsonRPCHttpOptionGroup.rpcHttpApis.contains(RpcApis.PERM.name()) + if (jsonRpcHttpOptions.getRpcHttpApis().contains(RpcApis.PERM.name()) || rpcWebsocketOptions.getRpcWsApis().contains(RpcApis.PERM.name())) { logger.warn( "Permissions are disabled. Cannot enable PERM APIs when not using Permissions."); @@ -2653,9 +2296,9 @@ public class BesuCommand implements DefaultCommandValues, Runnable { } private boolean anyPrivacyApiEnabled() { - return jsonRPCHttpOptionGroup.rpcHttpApis.contains(RpcApis.EEA.name()) + return jsonRpcHttpOptions.getRpcHttpApis().contains(RpcApis.EEA.name()) || rpcWebsocketOptions.getRpcWsApis().contains(RpcApis.EEA.name()) - || jsonRPCHttpOptionGroup.rpcHttpApis.contains(RpcApis.PRIV.name()) + || jsonRpcHttpOptions.getRpcHttpApis().contains(RpcApis.PRIV.name()) || rpcWebsocketOptions.getRpcWsApis().contains(RpcApis.PRIV.name()); } @@ -3018,15 +2661,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable { .orElseGet(() -> KeyPairUtil.getDefaultKeyFile(dataDir())); } - private String rpcHttpAuthenticationCredentialsFile() { - final String filename = jsonRPCHttpOptionGroup.rpcHttpAuthenticationCredentialsFile; - - if (filename != null) { - RpcAuthFileValidator.validate(commandLine, filename, "HTTP"); - } - return filename; - } - private String getDefaultPermissioningFilePath() { return dataDir() + System.getProperty("file.separator") @@ -3160,9 +2794,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { graphQlOptionGroup.graphQLHttpPort, graphQlOptionGroup.isGraphQLHttpEnabled); addPortIfEnabled( - effectivePorts, - jsonRPCHttpOptionGroup.rpcHttpPort, - jsonRPCHttpOptionGroup.isRpcHttpEnabled); + effectivePorts, jsonRpcHttpOptions.getRpcHttpPort(), jsonRpcHttpOptions.isRpcHttpEnabled()); addPortIfEnabled( effectivePorts, rpcWebsocketOptions.getRpcWsPort(), rpcWebsocketOptions.isRpcWsEnabled()); addPortIfEnabled(effectivePorts, engineRPCOptionGroup.engineRpcPort, isEngineApiEnabled()); @@ -3310,28 +2942,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable { return engineRPCOptionGroup.overrideEngineRpcEnabled || isMergeEnabled(); } - private static List getJDKEnabledCipherSuites() { - try { - final SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, null, null); - final SSLEngine engine = context.createSSLEngine(); - return Arrays.asList(engine.getEnabledCipherSuites()); - } catch (final KeyManagementException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - private static List getJDKEnabledProtocols() { - try { - final SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, null, null); - final SSLEngine engine = context.createSSLEngine(); - return Arrays.asList(engine.getEnabledProtocols()); - } catch (final KeyManagementException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - private SyncMode getDefaultSyncModeIfNotSet() { return Optional.ofNullable(syncMode) .orElse( diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/JsonRpcHttpOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/JsonRpcHttpOptions.java new file mode 100644 index 0000000000..51b6a59f1c --- /dev/null +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/JsonRpcHttpOptions.java @@ -0,0 +1,492 @@ +/* + * 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 java.util.Arrays.asList; +import static org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration.DEFAULT_JSON_RPC_PORT; +import static org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration.DEFAULT_PRETTY_JSON_ENABLED; +import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.DEFAULT_RPC_APIS; +import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.VALID_APIS; + +import org.hyperledger.besu.cli.DefaultCommandValues; +import org.hyperledger.besu.cli.custom.CorsAllowedOriginsProperty; +import org.hyperledger.besu.cli.custom.RpcAuthFileValidator; +import org.hyperledger.besu.cli.util.CommandLineUtils; +import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm; +import org.hyperledger.besu.ethereum.api.tls.FileBasedPasswordProvider; +import org.hyperledger.besu.ethereum.api.tls.TlsClientAuthConfiguration; +import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration; + +import java.io.File; +import java.nio.file.Path; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import com.google.common.base.Strings; +import org.slf4j.Logger; +import picocli.CommandLine; + +/** + * Handles configuration options for the JSON-RPC HTTP service, including validation and creation of + * a JSON-RPC configuration. + */ +public class JsonRpcHttpOptions { + @CommandLine.Option( + names = {"--rpc-http-enabled"}, + description = "Set to start the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") + private final Boolean isRpcHttpEnabled = false; + + @SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings. + @CommandLine.Option( + names = {"--rpc-http-host"}, + paramLabel = DefaultCommandValues.MANDATORY_HOST_FORMAT_HELP, + description = "Host for JSON-RPC HTTP to listen on (default: ${DEFAULT-VALUE})", + arity = "1") + private String rpcHttpHost; + + @CommandLine.Option( + names = {"--rpc-http-port"}, + paramLabel = DefaultCommandValues.MANDATORY_PORT_FORMAT_HELP, + description = "Port for JSON-RPC HTTP to listen on (default: ${DEFAULT-VALUE})", + arity = "1") + private final Integer rpcHttpPort = DEFAULT_JSON_RPC_PORT; + + @CommandLine.Option( + names = {"--rpc-http-max-active-connections"}, + description = + "Maximum number of HTTP connections allowed for JSON-RPC (default: ${DEFAULT-VALUE}). Once this limit is reached, incoming connections will be rejected.", + arity = "1") + private final Integer rpcHttpMaxConnections = DefaultCommandValues.DEFAULT_HTTP_MAX_CONNECTIONS; + + // A list of origins URLs that are accepted by the JsonRpcHttpServer (CORS) + @CommandLine.Option( + names = {"--rpc-http-cors-origins"}, + description = "Comma separated origin domain URLs for CORS validation (default: none)") + private final CorsAllowedOriginsProperty rpcHttpCorsAllowedOrigins = + new CorsAllowedOriginsProperty(); + + @CommandLine.Option( + names = {"--rpc-http-api", "--rpc-http-apis"}, + paramLabel = "", + split = " {0,1}, {0,1}", + arity = "1..*", + description = + "Comma separated list of APIs to enable on JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") + private final List rpcHttpApis = DEFAULT_RPC_APIS; + + @CommandLine.Option( + names = {"--rpc-http-api-method-no-auth", "--rpc-http-api-methods-no-auth"}, + paramLabel = "", + split = " {0,1}, {0,1}", + arity = "1..*", + description = + "Comma separated list of API methods to exclude from RPC authentication services, RPC HTTP authentication must be enabled") + private final List rpcHttpApiMethodsNoAuth = new ArrayList(); + + @CommandLine.Option( + names = {"--rpc-http-authentication-enabled"}, + description = + "Require authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") + private final Boolean isRpcHttpAuthenticationEnabled = false; + + @SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings. + @CommandLine.Option( + names = {"--rpc-http-authentication-credentials-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = + "Storage file for JSON-RPC HTTP authentication credentials (default: ${DEFAULT-VALUE})", + arity = "1") + private String rpcHttpAuthenticationCredentialsFile = null; + + @CommandLine.Option( + names = {"--rpc-http-authentication-jwt-public-key-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = "JWT public key file for JSON-RPC HTTP authentication", + arity = "1") + private final File rpcHttpAuthenticationPublicKeyFile = null; + + @CommandLine.Option( + names = {"--rpc-http-authentication-jwt-algorithm"}, + description = + "Encryption algorithm used for HTTP JWT public key. Possible values are ${COMPLETION-CANDIDATES}" + + " (default: ${DEFAULT-VALUE})", + arity = "1") + private final JwtAlgorithm rpcHttpAuthenticationAlgorithm = + DefaultCommandValues.DEFAULT_JWT_ALGORITHM; + + @CommandLine.Option( + names = {"--rpc-http-tls-enabled"}, + description = "Enable TLS for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") + private final Boolean isRpcHttpTlsEnabled = false; + + @CommandLine.Option( + names = {"--rpc-http-tls-keystore-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = + "Keystore (PKCS#12) containing key/certificate for the JSON-RPC HTTP service. Required if TLS is enabled.") + private final Path rpcHttpTlsKeyStoreFile = null; + + @CommandLine.Option( + names = {"--rpc-http-tls-keystore-password-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = + "File containing password to unlock keystore for the JSON-RPC HTTP service. Required if TLS is enabled.") + private final Path rpcHttpTlsKeyStorePasswordFile = null; + + @CommandLine.Option( + names = {"--rpc-http-tls-client-auth-enabled"}, + description = + "Enable TLS client authentication for the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") + private final Boolean isRpcHttpTlsClientAuthEnabled = false; + + @CommandLine.Option( + names = {"--rpc-http-tls-known-clients-file"}, + paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP, + description = + "Path to file containing clients certificate common name and fingerprint for client authentication") + private final Path rpcHttpTlsKnownClientsFile = null; + + @CommandLine.Option( + names = {"--rpc-http-tls-ca-clients-enabled"}, + description = + "Enable to accept clients certificate signed by a valid CA for client authentication (default: ${DEFAULT-VALUE})") + private final Boolean isRpcHttpTlsCAClientsEnabled = false; + + @CommandLine.Option( + names = {"--rpc-http-tls-protocol", "--rpc-http-tls-protocols"}, + description = "Comma separated list of TLS protocols to support (default: ${DEFAULT-VALUE})", + split = ",", + arity = "1..*") + private final List rpcHttpTlsProtocols = + new ArrayList<>(DefaultCommandValues.DEFAULT_TLS_PROTOCOLS); + + @CommandLine.Option( + names = {"--rpc-http-tls-cipher-suite", "--rpc-http-tls-cipher-suites"}, + description = "Comma separated list of TLS cipher suites to support", + split = ",", + arity = "1..*") + private final List rpcHttpTlsCipherSuites = new ArrayList<>(); + + @CommandLine.Option( + names = {"--rpc-http-max-batch-size"}, + paramLabel = DefaultCommandValues.MANDATORY_INTEGER_FORMAT_HELP, + description = + "Specifies the maximum number of requests in a single RPC batch request via RPC. -1 specifies no limit (default: ${DEFAULT-VALUE})") + private final Integer rpcHttpMaxBatchSize = DefaultCommandValues.DEFAULT_HTTP_MAX_BATCH_SIZE; + + @CommandLine.Option( + names = {"--rpc-http-max-request-content-length"}, + paramLabel = DefaultCommandValues.MANDATORY_LONG_FORMAT_HELP, + description = "Specifies the maximum request content length. (default: ${DEFAULT-VALUE})") + private final Long rpcHttpMaxRequestContentLength = + DefaultCommandValues.DEFAULT_MAX_REQUEST_CONTENT_LENGTH; + + @CommandLine.Option( + names = {"--json-pretty-print-enabled"}, + description = "Enable JSON pretty print format (default: ${DEFAULT-VALUE})") + private final Boolean prettyJsonEnabled = DEFAULT_PRETTY_JSON_ENABLED; + + /** + * Validates the Rpc Http 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 configuredApis) { + + if (!rpcHttpApis.stream().allMatch(configuredApis)) { + final List invalidHttpApis = new ArrayList<>(rpcHttpApis); + invalidHttpApis.removeAll(VALID_APIS); + throw new CommandLine.ParameterException( + commandLine, + "Invalid value for option '--rpc-http-api': invalid entries found " + invalidHttpApis); + } + + final boolean validHttpApiMethods = + rpcHttpApiMethodsNoAuth.stream().allMatch(RpcMethod::rpcMethodExists); + + if (!validHttpApiMethods) { + throw new CommandLine.ParameterException( + commandLine, + "Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods"); + } + + if (isRpcHttpAuthenticationEnabled) { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-http-authentication-public-key-file", + rpcHttpAuthenticationPublicKeyFile == null, + List.of("--rpc-http-authentication-jwt-algorithm")); + } + + if (isRpcHttpAuthenticationEnabled + && rpcHttpAuthenticationCredentialsFile(commandLine) == null + && rpcHttpAuthenticationPublicKeyFile == null) { + throw new CommandLine.ParameterException( + commandLine, + "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file"); + } + + checkDependencies(logger, commandLine); + + if (isRpcTlsConfigurationRequired()) { + validateTls(commandLine); + } + } + + /** + * Creates a JsonRpcConfiguration based on the provided options. + * + * @param hostsAllowlist List of hosts allowed + * @param defaultHostAddress Default host address + * @param timoutSec timeout in seconds + * @return A JsonRpcConfiguration instance + */ + public JsonRpcConfiguration jsonRpcConfiguration( + final List hostsAllowlist, final String defaultHostAddress, final Long timoutSec) { + + final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); + jsonRpcConfiguration.setEnabled(isRpcHttpEnabled); + jsonRpcConfiguration.setHost( + Strings.isNullOrEmpty(rpcHttpHost) ? defaultHostAddress : rpcHttpHost); + jsonRpcConfiguration.setPort(rpcHttpPort); + jsonRpcConfiguration.setMaxActiveConnections(rpcHttpMaxConnections); + jsonRpcConfiguration.setCorsAllowedDomains(rpcHttpCorsAllowedOrigins); + jsonRpcConfiguration.setRpcApis(rpcHttpApis.stream().distinct().collect(Collectors.toList())); + jsonRpcConfiguration.setNoAuthRpcApis( + rpcHttpApiMethodsNoAuth.stream().distinct().collect(Collectors.toList())); + jsonRpcConfiguration.setHostsAllowlist(hostsAllowlist); + jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled); + jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile); + jsonRpcConfiguration.setAuthenticationPublicKeyFile(rpcHttpAuthenticationPublicKeyFile); + jsonRpcConfiguration.setAuthenticationAlgorithm(rpcHttpAuthenticationAlgorithm); + jsonRpcConfiguration.setTlsConfiguration(rpcHttpTlsConfiguration()); + jsonRpcConfiguration.setHttpTimeoutSec(timoutSec); + jsonRpcConfiguration.setMaxBatchSize(rpcHttpMaxBatchSize); + jsonRpcConfiguration.setMaxRequestContentLength(rpcHttpMaxRequestContentLength); + jsonRpcConfiguration.setPrettyJsonEnabled(prettyJsonEnabled); + return jsonRpcConfiguration; + } + + /** + * Checks dependencies between options. + * + * @param logger Logger instance + * @param commandLine CommandLine instance + */ + public void checkDependencies(final Logger logger, final CommandLine commandLine) { + checkRpcTlsClientAuthOptionsDependencies(logger, commandLine); + checkRpcTlsOptionsDependencies(logger, commandLine); + checkRpcHttpOptionsDependencies(logger, commandLine); + } + + private void checkRpcTlsClientAuthOptionsDependencies( + final Logger logger, final CommandLine commandLine) { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-http-tls-client-auth-enabled", + !isRpcHttpTlsClientAuthEnabled, + asList("--rpc-http-tls-known-clients-file", "--rpc-http-tls-ca-clients-enabled")); + } + + private void checkRpcTlsOptionsDependencies(final Logger logger, final CommandLine commandLine) { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-http-tls-enabled", + !isRpcHttpTlsEnabled, + asList( + "--rpc-http-tls-keystore-file", + "--rpc-http-tls-keystore-password-file", + "--rpc-http-tls-client-auth-enabled", + "--rpc-http-tls-known-clients-file", + "--rpc-http-tls-ca-clients-enabled", + "--rpc-http-tls-protocols", + "--rpc-http-tls-cipher-suite", + "--rpc-http-tls-cipher-suites")); + } + + private void checkRpcHttpOptionsDependencies(final Logger logger, final CommandLine commandLine) { + CommandLineUtils.checkOptionDependencies( + logger, + commandLine, + "--rpc-http-enabled", + !isRpcHttpEnabled, + asList( + "--rpc-http-api", + "--rpc-http-apis", + "--rpc-http-api-method-no-auth", + "--rpc-http-api-methods-no-auth", + "--rpc-http-cors-origins", + "--rpc-http-host", + "--rpc-http-port", + "--rpc-http-max-active-connections", + "--rpc-http-authentication-enabled", + "--rpc-http-authentication-credentials-file", + "--rpc-http-authentication-public-key-file", + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + "--rpc-http-tls-keystore-password-file", + "--rpc-http-tls-client-auth-enabled", + "--rpc-http-tls-known-clients-file", + "--rpc-http-tls-ca-clients-enabled", + "--rpc-http-authentication-jwt-algorithm", + "--rpc-http-tls-protocols", + "--rpc-http-tls-cipher-suite", + "--rpc-http-tls-cipher-suites")); + } + + private void validateTls(final CommandLine commandLine) { + if (rpcHttpTlsKeyStoreFile == null) { + throw new CommandLine.ParameterException( + commandLine, "Keystore file is required when TLS is enabled for JSON-RPC HTTP endpoint"); + } + + if (rpcHttpTlsKeyStorePasswordFile == null) { + throw new CommandLine.ParameterException( + commandLine, + "File containing password to unlock keystore is required when TLS is enabled for JSON-RPC HTTP endpoint"); + } + + if (isRpcHttpTlsClientAuthEnabled + && !isRpcHttpTlsCAClientsEnabled + && rpcHttpTlsKnownClientsFile == null) { + throw new CommandLine.ParameterException( + commandLine, + "Known-clients file must be specified or CA clients must be enabled when TLS client authentication is enabled for JSON-RPC HTTP endpoint"); + } + + rpcHttpTlsProtocols.retainAll(getJDKEnabledProtocols()); + if (rpcHttpTlsProtocols.isEmpty()) { + throw new CommandLine.ParameterException( + commandLine, + "No valid TLS protocols specified (the following protocols are enabled: " + + getJDKEnabledProtocols() + + ")"); + } + + for (final String cipherSuite : rpcHttpTlsCipherSuites) { + if (!getJDKEnabledCipherSuites().contains(cipherSuite)) { + throw new CommandLine.ParameterException( + commandLine, "Invalid TLS cipher suite specified " + cipherSuite); + } + } + } + + private Optional rpcHttpTlsConfiguration() { + if (!isRpcTlsConfigurationRequired()) { + return Optional.empty(); + } + + rpcHttpTlsCipherSuites.retainAll(getJDKEnabledCipherSuites()); + + return Optional.of( + TlsConfiguration.Builder.aTlsConfiguration() + .withKeyStorePath(rpcHttpTlsKeyStoreFile) + .withKeyStorePasswordSupplier( + new FileBasedPasswordProvider(rpcHttpTlsKeyStorePasswordFile)) + .withClientAuthConfiguration(rpcHttpTlsClientAuthConfiguration()) + .withSecureTransportProtocols(rpcHttpTlsProtocols) + .withCipherSuites(rpcHttpTlsCipherSuites) + .build()); + } + + private boolean isRpcTlsConfigurationRequired() { + return isRpcHttpEnabled && isRpcHttpTlsEnabled; + } + + private TlsClientAuthConfiguration rpcHttpTlsClientAuthConfiguration() { + if (isRpcHttpTlsClientAuthEnabled) { + return TlsClientAuthConfiguration.Builder.aTlsClientAuthConfiguration() + .withKnownClientsFile(rpcHttpTlsKnownClientsFile) + .withCaClientsEnabled(isRpcHttpTlsCAClientsEnabled) + .build(); + } + + return null; + } + + private static List getJDKEnabledCipherSuites() { + try { + final SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, null, null); + final SSLEngine engine = context.createSSLEngine(); + return Arrays.asList(engine.getEnabledCipherSuites()); + } catch (final KeyManagementException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private static List getJDKEnabledProtocols() { + try { + final SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, null, null); + final SSLEngine engine = context.createSSLEngine(); + return Arrays.asList(engine.getEnabledProtocols()); + } catch (final KeyManagementException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private String rpcHttpAuthenticationCredentialsFile(final CommandLine commandLine) { + final String filename = rpcHttpAuthenticationCredentialsFile; + + if (filename != null) { + RpcAuthFileValidator.validate(commandLine, filename, "HTTP"); + } + return filename; + } + + /** + * Returns the list of APIs enabled for RPC over HTTP. + * + * @return A list of APIs + */ + public List getRpcHttpApis() { + return rpcHttpApis; + } + + /** + * Returns the port for RPC over HTTP. + * + * @return The port number + */ + public Integer getRpcHttpPort() { + return rpcHttpPort; + } + + /** + * Checks if RPC over HTTP is enabled. + * + * @return true if enabled, false otherwise + */ + public Boolean isRpcHttpEnabled() { + return isRpcHttpEnabled; + } +} diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index 938c25af9d..c69d886109 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -28,9 +28,6 @@ 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.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; -import static org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.PERM; import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.GOERLI_BOOTSTRAP_NODES; import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.GOERLI_DISCOVERY_URL; import static org.hyperledger.besu.ethereum.p2p.config.DefaultDiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES; @@ -45,7 +42,6 @@ import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -64,10 +60,7 @@ import org.hyperledger.besu.ethereum.api.ImmutableApiConfiguration; import org.hyperledger.besu.ethereum.api.graphql.GraphQLConfiguration; import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions; import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; -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 org.hyperledger.besu.ethereum.api.tls.TlsConfiguration; import org.hyperledger.besu.ethereum.core.MiningParameters; import org.hyperledger.besu.ethereum.core.PrivacyParameters; import org.hyperledger.besu.ethereum.eth.sync.SyncMode; @@ -87,7 +80,6 @@ import org.hyperledger.besu.nat.NatMethod; import org.hyperledger.besu.pki.config.PkiKeyStoreConfiguration; import org.hyperledger.besu.plugin.data.EnodeURL; import org.hyperledger.besu.plugin.services.privacy.PrivateMarkerTransactionFactory; -import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; import org.hyperledger.besu.util.number.Fraction; import org.hyperledger.besu.util.number.Percentage; import org.hyperledger.besu.util.number.PositiveNumber; @@ -96,7 +88,6 @@ import org.hyperledger.besu.util.platform.PlatformDetector; import java.io.File; import java.io.IOException; import java.math.BigInteger; -import java.net.ServerSocket; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -110,7 +101,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -1545,37 +1535,6 @@ public class BesuCommandTest extends CommandTestAbstract { verify(mockRunnerBuilder).build(); } - @Test - public void rpcHttpMaxBatchSizeOptionMustBeUsed() { - final int rpcHttpMaxBatchSize = 1; - parseCommand("--rpc-http-max-batch-size", Integer.toString(rpcHttpMaxBatchSize)); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getMaxBatchSize()) - .isEqualTo(rpcHttpMaxBatchSize); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpMaxRequestContentLengthOptionMustBeUsed() { - final int rpcHttpMaxRequestContentLength = 1; - parseCommand( - "--rpc-http-max-request-content-length", Long.toString(rpcHttpMaxRequestContentLength)); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getMaxRequestContentLength()) - .isEqualTo(rpcHttpMaxRequestContentLength); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - @Test public void maxpeersSet_p2pPeerLowerBoundSet() { @@ -1941,1046 +1900,267 @@ public class BesuCommandTest extends CommandTestAbstract { @Test public void natMethodFallbackEnabledPropertyIsCorrectlyUpdatedWithKubernetes() { - parseCommand("--nat-method", "KUBERNETES", "--Xnat-method-fallback-enabled", "false"); - verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(false)); - parseCommand("--nat-method", "KUBERNETES", "--Xnat-method-fallback-enabled", "true"); - verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(true)); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void natMethodFallbackEnabledPropertyIsCorrectlyUpdatedWithDocker() { - - parseCommand("--nat-method", "DOCKER", "--Xnat-method-fallback-enabled", "false"); - verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(false)); - parseCommand("--nat-method", "DOCKER", "--Xnat-method-fallback-enabled", "true"); - verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(true)); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void natMethodFallbackEnabledPropertyIsCorrectlyUpdatedWithUpnp() { - - parseCommand("--nat-method", "UPNP", "--Xnat-method-fallback-enabled", "false"); - verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(false)); - parseCommand("--nat-method", "UPNP", "--Xnat-method-fallback-enabled", "true"); - verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(true)); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void natMethodFallbackEnabledCannotBeUsedWithAutoMethod() { - parseCommand("--nat-method", "AUTO", "--Xnat-method-fallback-enabled", "false"); - Mockito.verifyNoInteractions(mockRunnerBuilder); - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains( - "The `--Xnat-method-fallback-enabled` parameter cannot be used in AUTO mode. Either remove --Xnat-method-fallback-enabled or select another mode (via --nat--method=XXXX)"); - } - - @Test - public void rpcHttpEnabledPropertyDefaultIsFalse() { - parseCommand(); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().isEnabled()).isFalse(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpEnabledPropertyMustBeUsed() { - parseCommand("--rpc-http-enabled"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().isEnabled()).isTrue(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void graphQLHttpEnabledPropertyDefaultIsFalse() { - parseCommand(); - - verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(graphQLConfigArgumentCaptor.getValue().isEnabled()).isFalse(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void graphQLHttpEnabledPropertyMustBeUsed() { - parseCommand("--graphql-http-enabled"); - - verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(graphQLConfigArgumentCaptor.getValue().isEnabled()).isTrue(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcApisPropertyMustBeUsed() { - parseCommand("--rpc-http-api", "ETH,NET,PERM", "--rpc-http-enabled"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - verify(mockLogger) - .warn("Permissions are disabled. Cannot enable PERM APIs when not using Permissions."); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) - .containsExactlyInAnyOrder(ETH.name(), NET.name(), PERM.name()); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcApisSupportsEngine() { - parseCommand("--rpc-http-api", "ENGINE", "--rpc-http-enabled"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) - .containsExactlyInAnyOrder(ENGINE.name()); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcApisPropertyIgnoresDuplicatesAndMustBeUsed() { - parseCommand("--rpc-http-api", "ETH,NET,NET", "--rpc-http-enabled"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) - .containsExactlyInAnyOrder(ETH.name(), NET.name()); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcApiNoAuthMethodsIgnoresDuplicatesAndMustBeUsed() { - parseCommand( - "--rpc-http-api-methods-no-auth", - "admin_peers, admin_peers, eth_getWork", - "--rpc-http-enabled"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getNoAuthRpcApis()) - .containsExactlyInAnyOrder( - RpcMethod.ADMIN_PEERS.getMethodName(), RpcMethod.ETH_GET_WORK.getMethodName()); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void engineApiAuthOptions() { - // TODO: once we have mainnet TTD, we can remove the TTD override parameter here - // https://github.com/hyperledger/besu/issues/3874 - parseCommand( - "--override-genesis-config", - "terminalTotalDifficulty=1337", - "--rpc-http-enabled", - "--engine-jwt-secret", - "/tmp/fakeKey.hex"); - verify(mockRunnerBuilder).engineJsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - assertThat(jsonRpcConfigArgumentCaptor.getValue().isAuthenticationEnabled()).isTrue(); - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void engineApiDisableAuthOptions() { - // TODO: once we have mainnet TTD, we can remove the TTD override parameter here - // https://github.com/hyperledger/besu/issues/3874 - parseCommand( - "--override-genesis-config", - "terminalTotalDifficulty=1337", - "--rpc-http-enabled", - "--engine-jwt-disabled", - "--engine-jwt-secret", - "/tmp/fakeKey.hex"); - verify(mockRunnerBuilder).engineJsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - assertThat(jsonRpcConfigArgumentCaptor.getValue().isAuthenticationEnabled()).isFalse(); - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpNoAuthApiMethodsCannotBeInvalid() { - parseCommand("--rpc-http-enabled", "--rpc-http-api-method-no-auth", "invalid"); - - Mockito.verifyNoInteractions(mockRunnerBuilder); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains( - "Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods"); - } - - @Test - public void rpcHttpOptionsRequiresServiceToBeEnabled() { - parseCommand( - "--rpc-http-api", - "ETH,NET", - "--rpc-http-host", - "0.0.0.0", - "--rpc-http-port", - "1234", - "--rpc-http-cors-origins", - "all", - "--rpc-http-max-active-connections", - "88"); - - verifyOptionsConstraintLoggerCall( - "--rpc-http-enabled", - "--rpc-http-host", - "--rpc-http-port", - "--rpc-http-cors-origins", - "--rpc-http-api", - "--rpc-http-max-active-connections"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpOptionsRequiresServiceToBeEnabledToml() throws IOException { - final Path toml = - createTempFile( - "toml", - "rpc-http-api=[\"ETH\",\"NET\"]\n" - + "rpc-http-host=\"0.0.0.0\"\n" - + "rpc-http-port=1234\n" - + "rpc-http-cors-origins=[\"all\"]\n" - + "rpc-http-max-active-connections=88"); - - parseCommand("--config-file", toml.toString()); - - verifyOptionsConstraintLoggerCall( - "--rpc-http-enabled", - "--rpc-http-host", - "--rpc-http-port", - "--rpc-http-cors-origins", - "--rpc-http-api", - "--rpc-http-max-active-connections"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void privacyTlsOptionsRequiresTlsToBeEnabled() { - when(storageService.getByName("rocksdb-privacy")) - .thenReturn(Optional.of(rocksDBSPrivacyStorageFactory)); - final URL configFile = this.getClass().getResource("/orion_publickey.pub"); - final String coinbaseStr = String.format("%040x", 1); - - parseCommand( - "--privacy-enabled", - "--miner-enabled", - "--miner-coinbase=" + coinbaseStr, - "--min-gas-price", - "0", - "--privacy-url", - ENCLAVE_URI, - "--privacy-public-key-file", - configFile.getPath(), - "--privacy-tls-keystore-file", - "/Users/me/key"); - - verifyOptionsConstraintLoggerCall("--privacy-tls-enabled", "--privacy-tls-keystore-file"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void privacyTlsOptionsRequiresTlsToBeEnabledToml() throws IOException { - when(storageService.getByName("rocksdb-privacy")) - .thenReturn(Optional.of(rocksDBSPrivacyStorageFactory)); - final URL configFile = this.getClass().getResource("/orion_publickey.pub"); - final String coinbaseStr = String.format("%040x", 1); - - final Path toml = - createTempFile( - "toml", - "privacy-enabled=true\n" - + "miner-enabled=true\n" - + "miner-coinbase=\"" - + coinbaseStr - + "\"\n" - + "min-gas-price=0\n" - + "privacy-url=\"" - + ENCLAVE_URI - + "\"\n" - + "privacy-public-key-file=\"" - + configFile.getPath() - + "\"\n" - + "privacy-tls-keystore-file=\"/Users/me/key\""); - - parseCommand("--config-file", toml.toString()); - - verifyOptionsConstraintLoggerCall("--privacy-tls-enabled", "--privacy-tls-keystore-file"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void privacyTlsOptionsRequiresPrivacyToBeEnabled() { - parseCommand("--privacy-tls-enabled", "--privacy-tls-keystore-file", "/Users/me/key"); - - verifyOptionsConstraintLoggerCall("--privacy-enabled", "--privacy-tls-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void privacyTlsOptionsRequiresPrivacyToBeEnabledToml() throws IOException { - final Path toml = - createTempFile( - "toml", "privacy-tls-enabled=true\n" + "privacy-tls-keystore-file=\"/Users/me/key\""); - - parseCommand("--config-file", toml.toString()); - - verifyOptionsConstraintLoggerCall("--privacy-enabled", "--privacy-tls-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcApisPropertyWithInvalidEntryMustDisplayError() { - parseCommand("--rpc-http-api", "BOB"); - - Mockito.verifyNoInteractions(mockRunnerBuilder); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - // PicoCLI uses longest option name for message when option has multiple names, so here plural. - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Invalid value for option '--rpc-http-api': invalid entries found [BOB]"); - } - - @Test - public void rpcApisPropertyWithPluginNamespaceAreValid() { - - rpcEndpointServiceImpl.registerRPCEndpoint( - "bob", "method", (Function) request -> "nothing"); - - parseCommand("--rpc-http-api", "BOB"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) - .containsExactlyInAnyOrder("BOB"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpHostAndPortOptionsMustBeUsed() { - - final String host = "1.2.3.4"; - final int port = 1234; - parseCommand( - "--rpc-http-enabled", "--rpc-http-host", host, "--rpc-http-port", String.valueOf(port)); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpHostMayBeLocalhost() { - - final String host = "localhost"; - parseCommand("--rpc-http-enabled", "--rpc-http-host", host); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpHostMayBeIPv6() { - - final String host = "2600:DB8::8545"; - parseCommand("--rpc-http-enabled", "--rpc-http-host", host); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpMaxActiveConnectionsPropertyMustBeUsed() { - final int maxConnections = 99; - parseCommand("--rpc-http-max-active-connections", String.valueOf(maxConnections)); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.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"); - - verifyOptionsConstraintLoggerCall("--rpc-http-enabled", "--rpc-http-tls-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsRequiresRpcHttpEnabledToml() throws IOException { - final Path toml = createTempFile("toml", "rpc-http-tls-enabled=true\n"); - - parseCommand("--config-file", toml.toString()); - - verifyOptionsConstraintLoggerCall("--rpc-http-enabled", "--rpc-http-tls-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsWithoutKeystoreReportsError() { - parseCommand("--rpc-http-enabled", "--rpc-http-tls-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Keystore file is required when TLS is enabled for JSON-RPC HTTP endpoint"); - } - - @Test - public void rpcHttpTlsWithoutPasswordfileReportsError() { - parseCommand( - "--rpc-http-enabled", - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - "/tmp/test.p12"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains( - "File containing password to unlock keystore is required when TLS is enabled for JSON-RPC HTTP endpoint"); - } - - @Test - public void rpcHttpTlsKeystoreAndPasswordMustBeUsed() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - final Optional tlsConfiguration = - jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); - assertThat(tlsConfiguration.isPresent()).isTrue(); - assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration().isEmpty()).isTrue(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsClientAuthWithoutKnownFileReportsError() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile, - "--rpc-http-tls-client-auth-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains( - "Known-clients file must be specified or CA clients must be enabled when TLS client authentication is enabled for JSON-RPC HTTP endpoint"); - } - - @Test - public void rpcHttpTlsClientAuthWithKnownClientFile() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - final String knownClientFile = "/tmp/knownClientFile"; - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile, - "--rpc-http-tls-client-auth-enabled", - "--rpc-http-tls-known-clients-file", - knownClientFile); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - final Optional tlsConfiguration = - jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); - assertThat(tlsConfiguration.isPresent()).isTrue(); - assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration().isPresent()).isTrue(); - assertThat( - tlsConfiguration.get().getClientAuthConfiguration().get().getKnownClientsFile().get()) - .isEqualTo(Path.of(knownClientFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled()) - .isFalse(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsClientAuthWithCAClient() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile, - "--rpc-http-tls-client-auth-enabled", - "--rpc-http-tls-ca-clients-enabled"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - final Optional tlsConfiguration = - jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); - assertThat(tlsConfiguration.isPresent()).isTrue(); - assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration().isPresent()).isTrue(); - assertThat( - tlsConfiguration - .get() - .getClientAuthConfiguration() - .get() - .getKnownClientsFile() - .isEmpty()) - .isTrue(); - assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled()) - .isTrue(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsClientAuthWithCAClientAndKnownClientFile() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - final String knownClientFile = "/tmp/knownClientFile"; - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile, - "--rpc-http-tls-client-auth-enabled", - "--rpc-http-tls-ca-clients-enabled", - "--rpc-http-tls-known-clients-file", - knownClientFile); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - final Optional tlsConfiguration = - jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); - assertThat(tlsConfiguration.isPresent()).isTrue(); - assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration().isPresent()).isTrue(); - assertThat( - tlsConfiguration.get().getClientAuthConfiguration().get().getKnownClientsFile().get()) - .isEqualTo(Path.of(knownClientFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled()) - .isTrue(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsCheckDefaultProtocolsAndCipherSuites() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - final Optional tlsConfiguration = - jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); - assertThat(tlsConfiguration).isPresent(); - assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration()).isEmpty(); - assertThat(tlsConfiguration.get().getCipherSuites().get()).isEmpty(); - assertThat(tlsConfiguration.get().getSecureTransportProtocols().get()) - .containsExactly("TLSv1.3", "TLSv1.2"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsCheckInvalidProtocols() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - final String protocol = "TLsv1.4"; - - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile, - "--rpc-http-tls-protocols", - protocol); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).contains("No valid TLS protocols specified"); - } - - @Test - public void rpcHttpTlsCheckInvalidCipherSuites() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - final String cipherSuites = "Invalid"; - - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile, - "--rpc-http-tls-cipher-suites", - cipherSuites); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Invalid TLS cipher suite specified " + cipherSuites); - } - - @Test - public void rpcHttpTlsCheckValidProtocolsAndCipherSuites() { - final String host = "1.2.3.4"; - final int port = 1234; - final String keystoreFile = "/tmp/test.p12"; - final String keystorePasswordFile = "/tmp/test.txt"; - final String protocols = "TLSv1.3,TLSv1.2"; - final String cipherSuites = - "TLS_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; - - parseCommand( - "--rpc-http-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-enabled", - "--rpc-http-tls-keystore-file", - keystoreFile, - "--rpc-http-tls-keystore-password-file", - keystorePasswordFile, - "--rpc-http-tls-protocols", - protocols, - "--rpc-http-tls-cipher-suites", - cipherSuites); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - final Optional tlsConfiguration = - jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); - assertThat(tlsConfiguration).isPresent(); - assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); - assertThat(tlsConfiguration.get().getClientAuthConfiguration()).isEmpty(); - assertThat(tlsConfiguration.get().getCipherSuites().get()) - .containsExactlyInAnyOrder( - "TLS_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); - assertThat(tlsConfiguration.get().getSecureTransportProtocols().get()) - .containsExactlyInAnyOrder("TLSv1.2", "TLSv1.3"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void rpcHttpTlsWarnIfCipherSuitesSpecifiedWithoutTls() { - final String host = "1.2.3.4"; - final int port = 1234; - final String cipherSuites = "Invalid"; - - parseCommand( - "--rpc-http-enabled", - "--engine-rpc-enabled", - "--rpc-http-host", - host, - "--rpc-http-port", - String.valueOf(port), - "--rpc-http-tls-cipher-suite", - cipherSuites); - verify( - mockLogger, - times(2)) // this is verified for both the full suite of apis, and the engine group. - .warn( - "{} has been ignored because {} was not defined on the command line.", - "--rpc-http-tls-cipher-suite", - "--rpc-http-tls-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void graphQLHttpHostAndPortOptionsMustBeUsed() { - - final String host = "1.2.3.4"; - final int port = 1234; - parseCommand( - "--graphql-http-enabled", - "--graphql-http-host", - host, - "--graphql-http-port", - String.valueOf(port)); - - verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(graphQLConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); - assertThat(graphQLConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + parseCommand("--nat-method", "KUBERNETES", "--Xnat-method-fallback-enabled", "false"); + verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(false)); + parseCommand("--nat-method", "KUBERNETES", "--Xnat-method-fallback-enabled", "true"); + verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(true)); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void graphQLHttpHostMayBeLocalhost() { - - final String host = "localhost"; - parseCommand("--graphql-http-enabled", "--graphql-http-host", host); - - verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); + public void natMethodFallbackEnabledPropertyIsCorrectlyUpdatedWithDocker() { - assertThat(graphQLConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + parseCommand("--nat-method", "DOCKER", "--Xnat-method-fallback-enabled", "false"); + verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(false)); + parseCommand("--nat-method", "DOCKER", "--Xnat-method-fallback-enabled", "true"); + verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(true)); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void graphQLHttpHostMayBeIPv6() { - - final String host = "2600:DB8::8545"; - parseCommand("--graphql-http-enabled", "--graphql-http-host", host); - - verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); + public void natMethodFallbackEnabledPropertyIsCorrectlyUpdatedWithUpnp() { - assertThat(graphQLConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + parseCommand("--nat-method", "UPNP", "--Xnat-method-fallback-enabled", "false"); + verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(false)); + parseCommand("--nat-method", "UPNP", "--Xnat-method-fallback-enabled", "true"); + verify(mockRunnerBuilder).natMethodFallbackEnabled(eq(true)); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsTwoDomainsMustBuildListWithBothDomains() { - final String[] origins = {"http://domain1.com", "https://domain2.com"}; - parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",", origins)); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) - .isEqualTo(origins); - + public void natMethodFallbackEnabledCannotBeUsedWithAutoMethod() { + parseCommand("--nat-method", "AUTO", "--Xnat-method-fallback-enabled", "false"); + Mockito.verifyNoInteractions(mockRunnerBuilder); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains( + "The `--Xnat-method-fallback-enabled` parameter cannot be used in AUTO mode. Either remove --Xnat-method-fallback-enabled or select another mode (via --nat--method=XXXX)"); } @Test - public void rpcHttpCorsOriginsDoubleCommaFilteredOut() { - final String[] origins = {"http://domain1.com", "https://domain2.com"}; - parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",,", origins)); + public void rpcHttpEnabledPropertyDefaultIsFalse() { + parseCommand(); verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); verify(mockRunnerBuilder).build(); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) - .isEqualTo(origins); + assertThat(jsonRpcConfigArgumentCaptor.getValue().isEnabled()).isFalse(); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsWithWildcardMustBuildListWithWildcard() { - final String[] origins = {"*"}; - parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",", origins)); + public void graphQLHttpEnabledPropertyDefaultIsFalse() { + parseCommand(); - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); verify(mockRunnerBuilder).build(); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) - .isEqualTo(origins); + assertThat(graphQLConfigArgumentCaptor.getValue().isEnabled()).isFalse(); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsWithAllMustBuildListWithWildcard() { - parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", "all"); + public void graphQLHttpEnabledPropertyMustBeUsed() { + parseCommand("--graphql-http-enabled"); - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); verify(mockRunnerBuilder).build(); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()).containsExactly("*"); + assertThat(graphQLConfigArgumentCaptor.getValue().isEnabled()).isTrue(); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsWithNoneMustBuildEmptyList() { - final String[] origins = {"none"}; - parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",", origins)); + public void rpcApisSupportsEngine() { + parseCommand("--rpc-http-api", "ENGINE", "--rpc-http-enabled"); verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); verify(mockRunnerBuilder).build(); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()).isEmpty(); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) + .containsExactlyInAnyOrder(ENGINE.name()); assertThat(commandOutput.toString(UTF_8)).isEmpty(); assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsNoneWithAnotherDomainMustFail() { - final String[] origins = {"http://domain1.com", "none"}; - parseCommand("--rpc-http-cors-origins", String.join(",", origins)); - - Mockito.verifyNoInteractions(mockRunnerBuilder); + public void engineApiAuthOptions() { + // TODO: once we have mainnet TTD, we can remove the TTD override parameter here + // https://github.com/hyperledger/besu/issues/3874 + parseCommand( + "--override-genesis-config", + "terminalTotalDifficulty=1337", + "--rpc-http-enabled", + "--engine-jwt-secret", + "/tmp/fakeKey.hex"); + verify(mockRunnerBuilder).engineJsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + assertThat(jsonRpcConfigArgumentCaptor.getValue().isAuthenticationEnabled()).isTrue(); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + @Test + public void engineApiDisableAuthOptions() { + // TODO: once we have mainnet TTD, we can remove the TTD override parameter here + // https://github.com/hyperledger/besu/issues/3874 + parseCommand( + "--override-genesis-config", + "terminalTotalDifficulty=1337", + "--rpc-http-enabled", + "--engine-jwt-disabled", + "--engine-jwt-secret", + "/tmp/fakeKey.hex"); + verify(mockRunnerBuilder).engineJsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + assertThat(jsonRpcConfigArgumentCaptor.getValue().isAuthenticationEnabled()).isFalse(); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Value 'none' can't be used with other domains"); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsNoneWithAnotherDomainMustFailNoneFirst() { - final String[] origins = {"none", "http://domain1.com"}; - parseCommand("--rpc-http-cors-origins", String.join(",", origins)); + public void privacyTlsOptionsRequiresTlsToBeEnabled() { + when(storageService.getByName("rocksdb-privacy")) + .thenReturn(Optional.of(rocksDBSPrivacyStorageFactory)); + final URL configFile = this.getClass().getResource("/orion_publickey.pub"); + final String coinbaseStr = String.format("%040x", 1); - Mockito.verifyNoInteractions(mockRunnerBuilder); + parseCommand( + "--privacy-enabled", + "--miner-enabled", + "--miner-coinbase=" + coinbaseStr, + "--min-gas-price", + "0", + "--privacy-url", + ENCLAVE_URI, + "--privacy-public-key-file", + configFile.getPath(), + "--privacy-tls-keystore-file", + "/Users/me/key"); + + verifyOptionsConstraintLoggerCall("--privacy-tls-enabled", "--privacy-tls-keystore-file"); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Value 'none' can't be used with other domains"); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsAllWithAnotherDomainMustFail() { - parseCommand("--rpc-http-cors-origins=http://domain1.com,all"); + public void privacyTlsOptionsRequiresTlsToBeEnabledToml() throws IOException { + when(storageService.getByName("rocksdb-privacy")) + .thenReturn(Optional.of(rocksDBSPrivacyStorageFactory)); + final URL configFile = this.getClass().getResource("/orion_publickey.pub"); + final String coinbaseStr = String.format("%040x", 1); - Mockito.verifyNoInteractions(mockRunnerBuilder); + final Path toml = + createTempFile( + "toml", + "privacy-enabled=true\n" + + "miner-enabled=true\n" + + "miner-coinbase=\"" + + coinbaseStr + + "\"\n" + + "min-gas-price=0\n" + + "privacy-url=\"" + + ENCLAVE_URI + + "\"\n" + + "privacy-public-key-file=\"" + + configFile.getPath() + + "\"\n" + + "privacy-tls-keystore-file=\"/Users/me/key\""); + + parseCommand("--config-file", toml.toString()); + + verifyOptionsConstraintLoggerCall("--privacy-tls-enabled", "--privacy-tls-keystore-file"); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Values '*' or 'all' can't be used with other domains"); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsAllWithAnotherDomainMustFailAsFlags() { - parseCommand("--rpc-http-cors-origins=http://domain1.com", "--rpc-http-cors-origins=all"); + public void privacyTlsOptionsRequiresPrivacyToBeEnabled() { + parseCommand("--privacy-tls-enabled", "--privacy-tls-keystore-file", "/Users/me/key"); - Mockito.verifyNoInteractions(mockRunnerBuilder); + verifyOptionsConstraintLoggerCall("--privacy-enabled", "--privacy-tls-enabled"); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Values '*' or 'all' can't be used with other domains"); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsWildcardWithAnotherDomainMustFail() { - parseCommand("--rpc-http-cors-origins=http://domain1.com,*"); + public void privacyTlsOptionsRequiresPrivacyToBeEnabledToml() throws IOException { + final Path toml = + createTempFile( + "toml", "privacy-tls-enabled=true\n" + "privacy-tls-keystore-file=\"/Users/me/key\""); - Mockito.verifyNoInteractions(mockRunnerBuilder); + parseCommand("--config-file", toml.toString()); + + verifyOptionsConstraintLoggerCall("--privacy-enabled", "--privacy-tls-enabled"); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Values '*' or 'all' can't be used with other domains"); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsWildcardWithAnotherDomainMustFailAsFlags() { - parseCommand("--rpc-http-cors-origins=http://domain1.com", "--rpc-http-cors-origins=*"); + public void graphQLHttpHostAndPortOptionsMustBeUsed() { - Mockito.verifyNoInteractions(mockRunnerBuilder); + final String host = "1.2.3.4"; + final int port = 1234; + parseCommand( + "--graphql-http-enabled", + "--graphql-http-host", + host, + "--graphql-http-port", + String.valueOf(port)); + + verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(graphQLConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Values '*' or 'all' can't be used with other domains"); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsInvalidRegexShouldFail() { - final String[] origins = {"**"}; - parseCommand("--rpc-http-cors-origins", String.join(",", origins)); + public void graphQLHttpHostMayBeLocalhost() { - Mockito.verifyNoInteractions(mockRunnerBuilder); + final String host = "localhost"; + parseCommand("--graphql-http-enabled", "--graphql-http-host", host); + + verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Domain values result in invalid regex pattern"); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } @Test - public void rpcHttpCorsOriginsEmptyValueFails() { - parseCommand("--rpc-http-cors-origins="); + public void graphQLHttpHostMayBeIPv6() { - Mockito.verifyNoInteractions(mockRunnerBuilder); + final String host = "2600:DB8::8545"; + parseCommand("--graphql-http-enabled", "--graphql-http-host", host); + + verify(mockRunnerBuilder).graphQLConfiguration(graphQLConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Domain cannot be empty string or null string."); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } /** test deprecated CLI option * */ @@ -3167,19 +2347,6 @@ public class BesuCommandTest extends CommandTestAbstract { .contains("Hostname cannot be empty string or null string."); } - @Test - public void rpcWsRpcEnabledPropertyDefaultIsFalse() { - parseCommand(); - - verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(wsRpcConfigArgumentCaptor.getValue().isEnabled()).isFalse(); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - @Test public void metricsEnabledPropertyDefaultIsFalse() { parseCommand(); @@ -4147,40 +3314,6 @@ public class BesuCommandTest extends CommandTestAbstract { .containsEntry(block2, Hash.fromHexStringLenient(hash2)); } - @Test - public void httpAuthenticationAlgorithIsConfigured() { - parseCommand("--rpc-http-authentication-jwt-algorithm", "ES256"); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getAuthenticationAlgorithm()) - .isEqualTo(JwtAlgorithm.ES256); - } - - @Test - public void httpAuthenticationPublicKeyIsConfigured() throws IOException { - final Path publicKey = Files.createTempFile("public_key", ""); - parseCommand("--rpc-http-authentication-jwt-public-key-file", publicKey.toString()); - - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - - assertThat(jsonRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath()) - .isEqualTo(publicKey.toString()); - } - - @Test - public void httpAuthenticationWithoutRequiredConfiguredOptionsMustFail() { - parseCommand("--rpc-http-enabled", "--rpc-http-authentication-enabled"); - - verifyNoInteractions(mockRunnerBuilder); - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains( - "Unable to authenticate JSON-RPC HTTP 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"); @@ -4412,29 +3545,6 @@ public class BesuCommandTest extends CommandTestAbstract { assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } - @Test - public void assertThatCheckPortClashAcceptsAsExpected() throws Exception { - // use WS port for HTTP - final int port = 8546; - parseCommand("--rpc-http-enabled", "--rpc-http-port", String.valueOf(port)); - verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); - verify(mockRunnerBuilder).build(); - assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); - } - - @Test - public void assertThatCheckPortClashRejectsAsExpected() throws Exception { - // use WS port for HTTP - final int port = 8546; - parseCommand("--rpc-http-enabled", "--rpc-http-port", String.valueOf(port), "--rpc-ws-enabled"); - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains( - "Port number '8546' has been specified multiple times. Please review the supplied configuration."); - } - @Test public void assertThatCheckPortClashRejectsAsExpectedForEngineApi() throws Exception { // use WS port for HTTP @@ -4638,19 +3748,6 @@ public class BesuCommandTest extends CommandTestAbstract { verify(mockLogger).warn("--sync-min-peers is ignored in FULL sync-mode"); } - @Test - public void portInUseReportsError() throws IOException { - final ServerSocket serverSocket = new ServerSocket(8545); - - parseCommandWithPortCheck("--rpc-http-enabled"); - - assertThat(commandOutput.toString(UTF_8)).isEmpty(); - assertThat(commandErrorOutput.toString(UTF_8)) - .contains("Port(s) '[8545]' already in use. Check for other processes using the port(s)."); - - serverSocket.close(); - } - @Test public void presentRequiredOptionShouldPass() { parseCommandWithRequiredOption("--accept-terms-and-conditions", "true"); diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptionsTest.java new file mode 100644 index 0000000000..83da024603 --- /dev/null +++ b/besu/src/test/java/org/hyperledger/besu/cli/options/JsonRpcHttpOptionsTest.java @@ -0,0 +1,926 @@ +/* + * 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.hyperledger.besu.ethereum.api.jsonrpc.RpcApis.PERM; +import static org.mockito.Mockito.times; +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.RpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.authentication.JwtAlgorithm; +import org.hyperledger.besu.ethereum.api.tls.TlsConfiguration; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; + +import java.io.IOException; +import java.net.ServerSocket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Function; + +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 JsonRpcHttpOptionsTest extends CommandTestAbstract { + + @Test + public void rpcHttpEnabledPropertyMustBeUsed() { + parseCommand("--rpc-http-enabled"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().isEnabled()).isTrue(); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcApisPropertyMustBeUsed() { + parseCommand("--rpc-http-api", "ETH,NET,PERM", "--rpc-http-enabled"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + verify(mockLogger) + .warn("Permissions are disabled. Cannot enable PERM APIs when not using Permissions."); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) + .containsExactlyInAnyOrder(ETH.name(), NET.name(), PERM.name()); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcApisPropertyIgnoresDuplicatesAndMustBeUsed() { + parseCommand("--rpc-http-api", "ETH,NET,NET", "--rpc-http-enabled"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) + .containsExactlyInAnyOrder(ETH.name(), NET.name()); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcApiNoAuthMethodsIgnoresDuplicatesAndMustBeUsed() { + parseCommand( + "--rpc-http-api-methods-no-auth", + "admin_peers, admin_peers, eth_getWork", + "--rpc-http-enabled"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getNoAuthRpcApis()) + .containsExactlyInAnyOrder( + RpcMethod.ADMIN_PEERS.getMethodName(), RpcMethod.ETH_GET_WORK.getMethodName()); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpNoAuthApiMethodsCannotBeInvalid() { + parseCommand("--rpc-http-enabled", "--rpc-http-api-method-no-auth", "invalid"); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains( + "Invalid value for option '--rpc-http-api-methods-no-auth', options must be valid RPC methods"); + } + + @Test + public void rpcHttpOptionsRequiresServiceToBeEnabled() { + parseCommand( + "--rpc-http-api", + "ETH,NET", + "--rpc-http-host", + "0.0.0.0", + "--rpc-http-port", + "1234", + "--rpc-http-cors-origins", + "all", + "--rpc-http-max-active-connections", + "88"); + + verifyOptionsConstraintLoggerCall( + "--rpc-http-enabled", + "--rpc-http-host", + "--rpc-http-port", + "--rpc-http-cors-origins", + "--rpc-http-api", + "--rpc-http-max-active-connections"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpOptionsRequiresServiceToBeEnabledToml() throws IOException { + final Path toml = + createTempFile( + "toml", + "rpc-http-api=[\"ETH\",\"NET\"]\n" + + "rpc-http-host=\"0.0.0.0\"\n" + + "rpc-http-port=1234\n" + + "rpc-http-cors-origins=[\"all\"]\n" + + "rpc-http-max-active-connections=88"); + + parseCommand("--config-file", toml.toString()); + + verifyOptionsConstraintLoggerCall( + "--rpc-http-enabled", + "--rpc-http-host", + "--rpc-http-port", + "--rpc-http-cors-origins", + "--rpc-http-api", + "--rpc-http-max-active-connections"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpHostAndPortOptionsMustBeUsed() { + + final String host = "1.2.3.4"; + final int port = 1234; + parseCommand( + "--rpc-http-enabled", "--rpc-http-host", host, "--rpc-http-port", String.valueOf(port)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpHostMayBeLocalhost() { + + final String host = "localhost"; + parseCommand("--rpc-http-enabled", "--rpc-http-host", host); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpHostMayBeIPv6() { + + final String host = "2600:DB8::8545"; + parseCommand("--rpc-http-enabled", "--rpc-http-host", host); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpMaxActiveConnectionsPropertyMustBeUsed() { + final int maxConnections = 99; + parseCommand("--rpc-http-max-active-connections", String.valueOf(maxConnections)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.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"); + + verifyOptionsConstraintLoggerCall("--rpc-http-enabled", "--rpc-http-tls-enabled"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsRequiresRpcHttpEnabledToml() throws IOException { + final Path toml = createTempFile("toml", "rpc-http-tls-enabled=true\n"); + + parseCommand("--config-file", toml.toString()); + + verifyOptionsConstraintLoggerCall("--rpc-http-enabled", "--rpc-http-tls-enabled"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsWithoutKeystoreReportsError() { + parseCommand("--rpc-http-enabled", "--rpc-http-tls-enabled"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Keystore file is required when TLS is enabled for JSON-RPC HTTP endpoint"); + } + + @Test + public void rpcHttpTlsWithoutPasswordfileReportsError() { + parseCommand( + "--rpc-http-enabled", + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + "/tmp/test.p12"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains( + "File containing password to unlock keystore is required when TLS is enabled for JSON-RPC HTTP endpoint"); + } + + @Test + public void rpcHttpTlsKeystoreAndPasswordMustBeUsed() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + final Optional tlsConfiguration = + jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); + assertThat(tlsConfiguration.isPresent()).isTrue(); + assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().isEmpty()).isTrue(); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsClientAuthWithoutKnownFileReportsError() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-client-auth-enabled"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains( + "Known-clients file must be specified or CA clients must be enabled when TLS client authentication is enabled for JSON-RPC HTTP endpoint"); + } + + @Test + public void rpcHttpTlsClientAuthWithKnownClientFile() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + final String knownClientFile = "/tmp/knownClientFile"; + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-client-auth-enabled", + "--rpc-http-tls-known-clients-file", + knownClientFile); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + final Optional tlsConfiguration = + jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); + assertThat(tlsConfiguration.isPresent()).isTrue(); + assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().isPresent()).isTrue(); + assertThat( + tlsConfiguration.get().getClientAuthConfiguration().get().getKnownClientsFile().get()) + .isEqualTo(Path.of(knownClientFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled()) + .isFalse(); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsClientAuthWithCAClient() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-client-auth-enabled", + "--rpc-http-tls-ca-clients-enabled"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + final Optional tlsConfiguration = + jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); + assertThat(tlsConfiguration.isPresent()).isTrue(); + assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().isPresent()).isTrue(); + assertThat( + tlsConfiguration + .get() + .getClientAuthConfiguration() + .get() + .getKnownClientsFile() + .isEmpty()) + .isTrue(); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled()) + .isTrue(); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsClientAuthWithCAClientAndKnownClientFile() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + final String knownClientFile = "/tmp/knownClientFile"; + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-client-auth-enabled", + "--rpc-http-tls-ca-clients-enabled", + "--rpc-http-tls-known-clients-file", + knownClientFile); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + final Optional tlsConfiguration = + jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); + assertThat(tlsConfiguration.isPresent()).isTrue(); + assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().isPresent()).isTrue(); + assertThat( + tlsConfiguration.get().getClientAuthConfiguration().get().getKnownClientsFile().get()) + .isEqualTo(Path.of(knownClientFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration().get().isCaClientsEnabled()) + .isTrue(); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsCheckDefaultProtocolsAndCipherSuites() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + final Optional tlsConfiguration = + jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); + assertThat(tlsConfiguration).isPresent(); + assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration()).isEmpty(); + assertThat(tlsConfiguration.get().getCipherSuites().get()).isEmpty(); + assertThat(tlsConfiguration.get().getSecureTransportProtocols().get()) + .containsExactly("TLSv1.3", "TLSv1.2"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsCheckInvalidProtocols() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + final String protocol = "TLsv1.4"; + + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-protocols", + protocol); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).contains("No valid TLS protocols specified"); + } + + @Test + public void rpcHttpTlsCheckInvalidCipherSuites() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + final String cipherSuites = "Invalid"; + + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-cipher-suites", + cipherSuites); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Invalid TLS cipher suite specified " + cipherSuites); + } + + @Test + public void rpcHttpTlsCheckValidProtocolsAndCipherSuites() { + final String host = "1.2.3.4"; + final int port = 1234; + final String keystoreFile = "/tmp/test.p12"; + final String keystorePasswordFile = "/tmp/test.txt"; + final String protocols = "TLSv1.3,TLSv1.2"; + final String cipherSuites = + "TLS_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + + parseCommand( + "--rpc-http-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-enabled", + "--rpc-http-tls-keystore-file", + keystoreFile, + "--rpc-http-tls-keystore-password-file", + keystorePasswordFile, + "--rpc-http-tls-protocols", + protocols, + "--rpc-http-tls-cipher-suites", + cipherSuites); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + final Optional tlsConfiguration = + jsonRpcConfigArgumentCaptor.getValue().getTlsConfiguration(); + assertThat(tlsConfiguration).isPresent(); + assertThat(tlsConfiguration.get().getKeyStorePath()).isEqualTo(Path.of(keystoreFile)); + assertThat(tlsConfiguration.get().getClientAuthConfiguration()).isEmpty(); + assertThat(tlsConfiguration.get().getCipherSuites().get()) + .containsExactlyInAnyOrder( + "TLS_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + assertThat(tlsConfiguration.get().getSecureTransportProtocols().get()) + .containsExactlyInAnyOrder("TLSv1.2", "TLSv1.3"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpTlsWarnIfCipherSuitesSpecifiedWithoutTls() { + final String host = "1.2.3.4"; + final int port = 1234; + final String cipherSuites = "Invalid"; + + parseCommand( + "--rpc-http-enabled", + "--engine-rpc-enabled", + "--rpc-http-host", + host, + "--rpc-http-port", + String.valueOf(port), + "--rpc-http-tls-cipher-suite", + cipherSuites); + verify( + mockLogger, + times(2)) // this is verified for both the full suite of apis, and the engine group. + .warn( + "{} has been ignored because {} was not defined on the command line.", + "--rpc-http-tls-cipher-suite", + "--rpc-http-tls-enabled"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpCorsOriginsTwoDomainsMustBuildListWithBothDomains() { + final String[] origins = {"http://domain1.com", "https://domain2.com"}; + parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",", origins)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) + .isEqualTo(origins); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpCorsOriginsDoubleCommaFilteredOut() { + final String[] origins = {"http://domain1.com", "https://domain2.com"}; + parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",,", origins)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) + .isEqualTo(origins); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpCorsOriginsWithWildcardMustBuildListWithWildcard() { + final String[] origins = {"*"}; + parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",", origins)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) + .isEqualTo(origins); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpCorsOriginsWithAllMustBuildListWithWildcard() { + parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", "all"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()).containsExactly("*"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpCorsOriginsWithNoneMustBuildEmptyList() { + final String[] origins = {"none"}; + parseCommand("--rpc-http-enabled", "--rpc-http-cors-origins", String.join(",", origins)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()).isEmpty(); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpCorsOriginsNoneWithAnotherDomainMustFail() { + final String[] origins = {"http://domain1.com", "none"}; + parseCommand("--rpc-http-cors-origins", String.join(",", origins)); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Value 'none' can't be used with other domains"); + } + + @Test + public void rpcHttpCorsOriginsNoneWithAnotherDomainMustFailNoneFirst() { + final String[] origins = {"none", "http://domain1.com"}; + parseCommand("--rpc-http-cors-origins", String.join(",", origins)); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Value 'none' can't be used with other domains"); + } + + @Test + public void rpcHttpCorsOriginsAllWithAnotherDomainMustFail() { + parseCommand("--rpc-http-cors-origins=http://domain1.com,all"); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Values '*' or 'all' can't be used with other domains"); + } + + @Test + public void rpcHttpCorsOriginsAllWithAnotherDomainMustFailAsFlags() { + parseCommand("--rpc-http-cors-origins=http://domain1.com", "--rpc-http-cors-origins=all"); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Values '*' or 'all' can't be used with other domains"); + } + + @Test + public void rpcHttpCorsOriginsWildcardWithAnotherDomainMustFail() { + parseCommand("--rpc-http-cors-origins=http://domain1.com,*"); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Values '*' or 'all' can't be used with other domains"); + } + + @Test + public void rpcHttpCorsOriginsWildcardWithAnotherDomainMustFailAsFlags() { + parseCommand("--rpc-http-cors-origins=http://domain1.com", "--rpc-http-cors-origins=*"); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Values '*' or 'all' can't be used with other domains"); + } + + @Test + public void rpcHttpCorsOriginsInvalidRegexShouldFail() { + final String[] origins = {"**"}; + parseCommand("--rpc-http-cors-origins", String.join(",", origins)); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Domain values result in invalid regex pattern"); + } + + @Test + public void rpcHttpCorsOriginsEmptyValueFails() { + parseCommand("--rpc-http-cors-origins="); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Domain cannot be empty string or null string."); + } + + @Test + public void rpcApisPropertyWithInvalidEntryMustDisplayError() { + parseCommand("--rpc-http-api", "BOB"); + + Mockito.verifyNoInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + // PicoCLI uses longest option name for message when option has multiple names, so here plural. + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Invalid value for option '--rpc-http-api': invalid entries found [BOB]"); + } + + @Test + public void rpcApisPropertyWithPluginNamespaceAreValid() { + + rpcEndpointServiceImpl.registerRPCEndpoint( + "bob", "method", (Function) request -> "nothing"); + + parseCommand("--rpc-http-api", "BOB"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getRpcApis()) + .containsExactlyInAnyOrder("BOB"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpMaxRequestContentLengthOptionMustBeUsed() { + final int rpcHttpMaxRequestContentLength = 1; + parseCommand( + "--rpc-http-max-request-content-length", Long.toString(rpcHttpMaxRequestContentLength)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getMaxRequestContentLength()) + .isEqualTo(rpcHttpMaxRequestContentLength); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void rpcHttpMaxBatchSizeOptionMustBeUsed() { + final int rpcHttpMaxBatchSize = 1; + parseCommand("--rpc-http-max-batch-size", Integer.toString(rpcHttpMaxBatchSize)); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getMaxBatchSize()) + .isEqualTo(rpcHttpMaxBatchSize); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void portInUseReportsError() throws IOException { + final ServerSocket serverSocket = new ServerSocket(8545); + + parseCommandWithPortCheck("--rpc-http-enabled"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Port(s) '[8545]' already in use. Check for other processes using the port(s)."); + + serverSocket.close(); + } + + @Test + public void assertThatCheckPortClashRejectsAsExpected() throws Exception { + // use WS port for HTTP + final int port = 8546; + parseCommand("--rpc-http-enabled", "--rpc-http-port", String.valueOf(port), "--rpc-ws-enabled"); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains( + "Port number '8546' has been specified multiple times. Please review the supplied configuration."); + } + + @Test + public void assertThatCheckPortClashAcceptsAsExpected() throws Exception { + // use WS port for HTTP + final int port = 8546; + parseCommand("--rpc-http-enabled", "--rpc-http-port", String.valueOf(port)); + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void httpAuthenticationWithoutRequiredConfiguredOptionsMustFail() { + parseCommand("--rpc-http-enabled", "--rpc-http-authentication-enabled"); + + verifyNoInteractions(mockRunnerBuilder); + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains( + "Unable to authenticate JSON-RPC HTTP endpoint without a supplied credentials file or authentication public key file"); + } + + @Test + public void httpAuthenticationAlgorithIsConfigured() { + parseCommand("--rpc-http-authentication-jwt-algorithm", "ES256"); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getAuthenticationAlgorithm()) + .isEqualTo(JwtAlgorithm.ES256); + } + + @Test + public void httpAuthenticationPublicKeyIsConfigured() throws IOException { + final Path publicKey = Files.createTempFile("public_key", ""); + parseCommand("--rpc-http-authentication-jwt-public-key-file", publicKey.toString()); + + verify(mockRunnerBuilder).jsonRpcConfiguration(jsonRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getAuthenticationPublicKeyFile().getPath()) + .isEqualTo(publicKey.toString()); + } +} diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/RpcWebsocketOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/RpcWebsocketOptionsTest.java index 0aaf01622e..0583afd7cc 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/options/RpcWebsocketOptionsTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/options/RpcWebsocketOptionsTest.java @@ -246,4 +246,17 @@ public class RpcWebsocketOptionsTest extends CommandTestAbstract { .contains( "Unable to authenticate JSON-RPC WebSocket endpoint without a supplied credentials file or authentication public key file"); } + + @Test + public void rpcWsRpcEnabledPropertyDefaultIsFalse() { + parseCommand(); + + verify(mockRunnerBuilder).webSocketConfiguration(wsRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(wsRpcConfigArgumentCaptor.getValue().isEnabled()).isFalse(); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } } diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/statemachine/BftFinalState.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/statemachine/BftFinalState.java index 13f6959779..0a65acc865 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/statemachine/BftFinalState.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/statemachine/BftFinalState.java @@ -28,8 +28,14 @@ import org.hyperledger.besu.datatypes.Address; import java.time.Clock; import java.util.Collection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** This is the full data set, or context, required for many of the aspects of BFT workflows. */ public class BftFinalState { + + private static final Logger LOG = LoggerFactory.getLogger(BftFinalState.class); + private final ValidatorProvider validatorProvider; private final NodeKey nodeKey; private final Address localAddress; @@ -126,7 +132,9 @@ public class BftFinalState { * @return the boolean */ public boolean isLocalNodeValidator() { - return getValidators().contains(localAddress); + final boolean isValidator = getValidators().contains(localAddress); + LOG.debug(isValidator ? "Local node is a validator" : "Local node is a non-validator"); + return isValidator; } /** diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/consensus/merge/ForkchoiceEvent.java b/ethereum/eth/src/main/java/org/hyperledger/besu/consensus/merge/ForkchoiceEvent.java index bc38cd3880..8207e13f75 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/consensus/merge/ForkchoiceEvent.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/consensus/merge/ForkchoiceEvent.java @@ -91,8 +91,6 @@ public class ForkchoiceEvent { + safeBlockHash + ", finalizedBlockHash=" + finalizedBlockHash - + ", safeBlockHash=" - + safeBlockHash + '}'; } } diff --git a/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoScalar.java b/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoScalar.java index 0e3356c379..55e3a84e0b 100644 --- a/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoScalar.java +++ b/evm/src/main/java/org/hyperledger/besu/collections/undo/UndoScalar.java @@ -61,7 +61,7 @@ public class UndoScalar implements Undoable { } /** - * Has this scalar had any change since the inital value + * Has this scalar had any change since the initial value * * @return true if there are any changes to undo */ diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index c72ad93433..01ed8833da 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -69,7 +69,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'ZsovOR0oPfomcLP4b+HjikWzM0Tx6sCwi68mf5qwZf4=' + knownHash = 'VpNy2KuAtEUc9hPguNivbjwy2YM3vIF444RCREJojqY=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/trielogs/TrieLogAccumulator.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/trielogs/TrieLogAccumulator.java index 6984ca48a1..384a327166 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/trielogs/TrieLogAccumulator.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/trielogs/TrieLogAccumulator.java @@ -23,7 +23,7 @@ import java.util.Map; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; -/** Accumulator interface tor provding trie updates for creating TrieLogs. */ +/** Accumulator interface for providing trie updates for creating TrieLogs. */ public interface TrieLogAccumulator { /**