mirror of https://github.com/hyperledger/besu
commit
6620e1d951
@ -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 = "<api name>", |
||||
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<String> rpcHttpApis = DEFAULT_RPC_APIS; |
||||
|
||||
@CommandLine.Option( |
||||
names = {"--rpc-http-api-method-no-auth", "--rpc-http-api-methods-no-auth"}, |
||||
paramLabel = "<api name>", |
||||
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<String> rpcHttpApiMethodsNoAuth = new ArrayList<String>(); |
||||
|
||||
@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<String> 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<String> 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<String> configuredApis) { |
||||
|
||||
if (!rpcHttpApis.stream().allMatch(configuredApis)) { |
||||
final List<String> 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<String> 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<TlsConfiguration> 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<String> 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<String> 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<String> 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; |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -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> 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> 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> 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> 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> 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> 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<PluginRpcRequest, Object>) 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()); |
||||
} |
||||
} |
Loading…
Reference in new issue