mirror of
https://github.com/vacp2p/linea-besu.git
synced 2026-01-09 21:17:54 -05:00
Add TLS/mTLS options and configure the GraphQL HTTP service (#7910)
* Add TLS/mTLS options and configure the GraphQL HTTP service Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io> * Update ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com> Signed-off-by: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com> * moved changelog entry to unreleased Signed-off-by: Sally MacFarlane <macfarla.github@gmail.com> --------- Signed-off-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io> Signed-off-by: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com> Signed-off-by: Sally MacFarlane <macfarla.github@gmail.com> Co-authored-by: Bhanu Pulluri <bhanu.pulluri@kaleido.io> Co-authored-by: Sally MacFarlane <macfarla.github@gmail.com>
This commit is contained in:
@@ -15,9 +15,10 @@
|
||||
- Proof of Work consensus
|
||||
- Fast Sync
|
||||
### Additions and Improvements
|
||||
- Add TLS/mTLS options and configure the GraphQL HTTP service[#7910](https://github.com/hyperledger/besu/pull/7910)
|
||||
### Bug fixes
|
||||
- Upgrade Netty to version 4.1.118 to fix CVE-2025-24970 [#8275](https://github.com/hyperledger/besu/pull/8275)
|
||||
- Added missing RPC method `debug_accountRange` to `RpcMethod.java` and implemented its handler. [#8153](https://github.com/hyperledger/besu/issues/8153)
|
||||
- Add missing RPC method `debug_accountRange` to `RpcMethod.java` and implemented its handler. [#8153](https://github.com/hyperledger/besu/issues/8153)
|
||||
|
||||
## 25.2.0
|
||||
|
||||
@@ -39,6 +40,7 @@
|
||||
- Add a tx selector to skip txs from the same sender after the first not selected [#8216](https://github.com/hyperledger/besu/pull/8216)
|
||||
- `rpc-gas-cap` default value has changed from 0 (unlimited) to 50M [#8251](https://github.com/hyperledger/besu/issues/8251)
|
||||
|
||||
|
||||
#### Prague
|
||||
- Add timestamps to enable Prague hardfork on Sepolia and Holesky test networks [#8163](https://github.com/hyperledger/besu/pull/8163)
|
||||
- Update system call addresses to match [devnet-6](https://github.com/ethereum/execution-spec-tests/releases/) values [#8209](https://github.com/hyperledger/besu/issues/8209)
|
||||
|
||||
@@ -57,6 +57,36 @@ public class GraphQlOptions {
|
||||
private final CorsAllowedOriginsProperty graphQLHttpCorsAllowedOrigins =
|
||||
new CorsAllowedOriginsProperty();
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--graphql-tls-enabled"},
|
||||
description = "Enable TLS for GraphQL HTTP service")
|
||||
private Boolean graphqlTlsEnabled = false;
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--graphql-tls-keystore-file"},
|
||||
description = "Path to the TLS keystore file for GraphQL HTTP service")
|
||||
private String graphqlTlsKeystoreFile;
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--graphql-tls-keystore-password-file"},
|
||||
description = "Path to the file containing the password for the TLS keystore")
|
||||
private String graphqlTlsKeystorePasswordFile;
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--graphql-mtls-enabled"},
|
||||
description = "Enable mTLS for GraphQL HTTP service")
|
||||
private Boolean graphqlMtlsEnabled = false;
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--graphql-tls-truststore-file"},
|
||||
description = "Path to the TLS truststore file for GraphQL HTTP service")
|
||||
private String graphqlTlsTruststoreFile;
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--graphql-tls-truststore-password-file"},
|
||||
description = "Path to the file containing the password for the TLS truststore")
|
||||
private String graphqlTlsTruststorePasswordFile;
|
||||
|
||||
/** Default constructor */
|
||||
public GraphQlOptions() {}
|
||||
|
||||
@@ -72,7 +102,28 @@ public class GraphQlOptions {
|
||||
commandLine,
|
||||
"--graphql-http-enabled",
|
||||
!isGraphQLHttpEnabled,
|
||||
asList("--graphql-http-cors-origins", "--graphql-http-host", "--graphql-http-port"));
|
||||
asList(
|
||||
"--graphql-http-cors-origins",
|
||||
"--graphql-http-host",
|
||||
"--graphql-http-port",
|
||||
"--graphql-tls-enabled"));
|
||||
|
||||
CommandLineUtils.checkOptionDependencies(
|
||||
logger,
|
||||
commandLine,
|
||||
"--graphql-tls-enabled",
|
||||
!graphqlTlsEnabled,
|
||||
asList(
|
||||
"--graphql-tls-keystore-file",
|
||||
"--graphql-tls-keystore-password-file",
|
||||
"--graphql-mtls-enabled"));
|
||||
|
||||
CommandLineUtils.checkOptionDependencies(
|
||||
logger,
|
||||
commandLine,
|
||||
"--graphql-mtls-enabled",
|
||||
!graphqlMtlsEnabled,
|
||||
asList("--graphql-tls-truststore-file", "--graphql-tls-truststore-password-file"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +144,13 @@ public class GraphQlOptions {
|
||||
graphQLConfiguration.setHostsAllowlist(hostsAllowlist);
|
||||
graphQLConfiguration.setCorsAllowedDomains(graphQLHttpCorsAllowedOrigins);
|
||||
graphQLConfiguration.setHttpTimeoutSec(timoutSec);
|
||||
graphQLConfiguration.setTlsEnabled(graphqlTlsEnabled);
|
||||
graphQLConfiguration.setTlsKeyStorePath(graphqlTlsKeystoreFile);
|
||||
graphQLConfiguration.setTlsKeyStorePasswordFile(graphqlTlsKeystorePasswordFile);
|
||||
graphQLConfiguration.setMtlsEnabled(graphqlMtlsEnabled);
|
||||
graphQLConfiguration.setTlsTrustStorePath(graphqlTlsTruststoreFile);
|
||||
graphQLConfiguration.setTlsTrustStorePasswordFile(graphqlTlsTruststorePasswordFile);
|
||||
|
||||
return graphQLConfiguration;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,12 @@ graphql-http-enabled=false
|
||||
graphql-http-host="6.7.8.9"
|
||||
graphql-http-port=6789
|
||||
graphql-http-cors-origins=["none"]
|
||||
graphql-tls-enabled=false
|
||||
graphql-tls-keystore-file="none.pfx"
|
||||
graphql-tls-keystore-password-file="none.passwd"
|
||||
graphql-mtls-enabled=false
|
||||
graphql-tls-truststore-file="none.pfx"
|
||||
graphql-tls-truststore-password-file="none.passwd"
|
||||
|
||||
# WebSockets API
|
||||
rpc-ws-enabled=false
|
||||
|
||||
@@ -18,6 +18,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import org.hyperledger.besu.ethereum.api.handlers.TimeoutOptions;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -44,6 +47,13 @@ public class GraphQLConfiguration {
|
||||
private List<String> hostsAllowlist = Arrays.asList("localhost", DEFAULT_GRAPHQL_HTTP_HOST);
|
||||
private long httpTimeoutSec = TimeoutOptions.defaultOptions().getTimeoutSeconds();
|
||||
|
||||
private String tlsKeyStorePath;
|
||||
private String tlsKeyStorePasswordFile;
|
||||
private String tlsTrustStorePath;
|
||||
private String tlsTrustStorePasswordFile;
|
||||
private boolean tlsEnabled;
|
||||
private boolean mtlsEnabled;
|
||||
|
||||
/**
|
||||
* Creates a default configuration for GraphQL.
|
||||
*
|
||||
@@ -174,6 +184,120 @@ public class GraphQLConfiguration {
|
||||
this.httpTimeoutSec = httpTimeoutSec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the TLS key store path.
|
||||
*
|
||||
* @return the TLS key store path
|
||||
*/
|
||||
public String getTlsKeyStorePath() {
|
||||
return tlsKeyStorePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the TLS key store path.
|
||||
*
|
||||
* @param tlsKeyStorePath the path to the TLS key store
|
||||
*/
|
||||
public void setTlsKeyStorePath(final String tlsKeyStorePath) {
|
||||
this.tlsKeyStorePath = tlsKeyStorePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the TLS key store password.
|
||||
*
|
||||
* @return the TLS key store password
|
||||
* @throws Exception if an error occurs while reading the password file
|
||||
*/
|
||||
public String getTlsKeyStorePassword() throws Exception {
|
||||
return new String(
|
||||
Files.readAllBytes(Paths.get(tlsKeyStorePasswordFile)), Charset.defaultCharset())
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the TLS key store password file.
|
||||
*
|
||||
* @param tlsKeyStorePasswordFile the path to the TLS key store password file
|
||||
*/
|
||||
public void setTlsKeyStorePasswordFile(final String tlsKeyStorePasswordFile) {
|
||||
this.tlsKeyStorePasswordFile = tlsKeyStorePasswordFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the TLS trust store path.
|
||||
*
|
||||
* @return the TLS trust store path
|
||||
*/
|
||||
public String getTlsTrustStorePath() {
|
||||
return tlsTrustStorePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the TLS trust store path.
|
||||
*
|
||||
* @param tlsTrustStorePath the path to the TLS trust store
|
||||
*/
|
||||
public void setTlsTrustStorePath(final String tlsTrustStorePath) {
|
||||
this.tlsTrustStorePath = tlsTrustStorePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the TLS trust store password.
|
||||
*
|
||||
* @return the TLS trust store password
|
||||
* @throws Exception if an error occurs while reading the password file
|
||||
*/
|
||||
public String getTlsTrustStorePassword() throws Exception {
|
||||
return new String(
|
||||
Files.readAllBytes(Paths.get(tlsTrustStorePasswordFile)), Charset.defaultCharset())
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the TLS trust store password file.
|
||||
*
|
||||
* @param tlsTrustStorePasswordFile the path to the TLS trust store password file
|
||||
*/
|
||||
public void setTlsTrustStorePasswordFile(final String tlsTrustStorePasswordFile) {
|
||||
this.tlsTrustStorePasswordFile = tlsTrustStorePasswordFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the TLS enabled status.
|
||||
*
|
||||
* @return true if TLS is enabled, false otherwise
|
||||
*/
|
||||
public boolean isTlsEnabled() {
|
||||
return tlsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the TLS enabled status.
|
||||
*
|
||||
* @param tlsEnabled the status to set. true to enable TLS, false to disable it
|
||||
*/
|
||||
public void setTlsEnabled(final boolean tlsEnabled) {
|
||||
this.tlsEnabled = tlsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mTLS enabled status.
|
||||
*
|
||||
* @return true if mTLS is enabled, false otherwise
|
||||
*/
|
||||
public boolean isMtlsEnabled() {
|
||||
return mtlsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mTLS enabled status.
|
||||
*
|
||||
* @param mtlsEnabled the status to set. true to enable mTLS, false to disable it
|
||||
*/
|
||||
public void setMtlsEnabled(final boolean mtlsEnabled) {
|
||||
this.mtlsEnabled = mtlsEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
|
||||
@@ -51,6 +51,7 @@ import graphql.GraphQLError;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.ClientAuth;
|
||||
import io.vertx.core.http.HttpHeaders;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.core.http.HttpServerOptions;
|
||||
@@ -60,6 +61,7 @@ import io.vertx.core.json.DecodeException;
|
||||
import io.vertx.core.json.Json;
|
||||
import io.vertx.core.json.jackson.JacksonCodec;
|
||||
import io.vertx.core.net.HostAndPort;
|
||||
import io.vertx.core.net.JksOptions;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.handler.BodyHandler;
|
||||
@@ -147,13 +149,43 @@ public class GraphQLHttpService {
|
||||
public CompletableFuture<?> start() {
|
||||
LOG.info("Starting GraphQL HTTP service on {}:{}", config.getHost(), config.getPort());
|
||||
// Create the HTTP server and a router object.
|
||||
httpServer =
|
||||
vertx.createHttpServer(
|
||||
new HttpServerOptions()
|
||||
.setHost(config.getHost())
|
||||
.setPort(config.getPort())
|
||||
.setHandle100ContinueAutomatically(true)
|
||||
.setCompressionSupported(true));
|
||||
HttpServerOptions options =
|
||||
new HttpServerOptions()
|
||||
.setHost(config.getHost())
|
||||
.setPort(config.getPort())
|
||||
.setHandle100ContinueAutomatically(true)
|
||||
.setCompressionSupported(true);
|
||||
|
||||
if (config.isTlsEnabled()) {
|
||||
try {
|
||||
options
|
||||
.setSsl(true)
|
||||
.setKeyCertOptions(
|
||||
new JksOptions()
|
||||
.setPath(config.getTlsKeyStorePath())
|
||||
.setPassword(config.getTlsKeyStorePassword()));
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get TLS keystore password", e);
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
|
||||
if (config.isMtlsEnabled()) {
|
||||
try {
|
||||
options
|
||||
.setTrustOptions(
|
||||
new JksOptions()
|
||||
.setPath(config.getTlsTrustStorePath())
|
||||
.setPassword(config.getTlsTrustStorePassword()))
|
||||
.setClientAuth(ClientAuth.REQUIRED);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to get TLS truststore password", e);
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Options {}", options);
|
||||
httpServer = vertx.createHttpServer(options);
|
||||
|
||||
// Handle graphql http requests
|
||||
final Router router = Router.router(vertx);
|
||||
@@ -303,7 +335,8 @@ public class GraphQLHttpService {
|
||||
if (httpServer == null) {
|
||||
return "";
|
||||
}
|
||||
return NetworkUtility.urlForSocketAddress("http", socketAddress());
|
||||
String scheme = config.isTlsEnabled() ? "https" : "http";
|
||||
return NetworkUtility.urlForSocketAddress(scheme, socketAddress());
|
||||
}
|
||||
|
||||
// Empty Get/Post requests to / will be redirected to /graphql using 308 Permanent Redirect
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
/*
|
||||
* Copyright contributors to Besu.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package org.hyperledger.besu.ethereum.api.graphql;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.hyperledger.besu.datatypes.Hash;
|
||||
import org.hyperledger.besu.datatypes.Wei;
|
||||
import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata;
|
||||
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
|
||||
import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata;
|
||||
import org.hyperledger.besu.ethereum.blockcreation.PoWMiningCoordinator;
|
||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||
import org.hyperledger.besu.ethereum.core.Synchronizer;
|
||||
import org.hyperledger.besu.ethereum.eth.EthProtocol;
|
||||
import org.hyperledger.besu.ethereum.eth.manager.EthScheduler;
|
||||
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
|
||||
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
|
||||
import org.hyperledger.besu.testutil.BlockTestUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.Writer;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.apache.tuweni.bytes.Bytes;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class GraphQLHttpsServiceTest {
|
||||
|
||||
// this tempDir is deliberately static
|
||||
@TempDir private static Path folder;
|
||||
|
||||
private static final Vertx vertx = Vertx.vertx();
|
||||
|
||||
private static GraphQLHttpService service;
|
||||
private static OkHttpClient client;
|
||||
private static String baseUrl;
|
||||
protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
protected static final MediaType GRAPHQL = MediaType.parse("application/graphql; charset=utf-8");
|
||||
private static BlockchainQueries blockchainQueries;
|
||||
private static GraphQL graphQL;
|
||||
private static Map<GraphQLContextType, Object> graphQlContextMap;
|
||||
private static PoWMiningCoordinator miningCoordinatorMock;
|
||||
|
||||
private final GraphQLTestHelper testHelper = new GraphQLTestHelper();
|
||||
// Generate a self-signed certificate
|
||||
private static SelfSignedCertificate ssc;
|
||||
private static SelfSignedCertificate clientSsc;
|
||||
|
||||
@BeforeAll
|
||||
public static void initServerAndClient() throws Exception {
|
||||
blockchainQueries = Mockito.mock(BlockchainQueries.class);
|
||||
final Synchronizer synchronizer = Mockito.mock(Synchronizer.class);
|
||||
graphQL = Mockito.mock(GraphQL.class);
|
||||
ssc = new SelfSignedCertificate();
|
||||
clientSsc = new SelfSignedCertificate();
|
||||
|
||||
miningCoordinatorMock = Mockito.mock(PoWMiningCoordinator.class);
|
||||
graphQlContextMap =
|
||||
Map.of(
|
||||
GraphQLContextType.BLOCKCHAIN_QUERIES,
|
||||
blockchainQueries,
|
||||
GraphQLContextType.TRANSACTION_POOL,
|
||||
Mockito.mock(TransactionPool.class),
|
||||
GraphQLContextType.MINING_COORDINATOR,
|
||||
miningCoordinatorMock,
|
||||
GraphQLContextType.SYNCHRONIZER,
|
||||
synchronizer);
|
||||
|
||||
final Set<Capability> supportedCapabilities = new HashSet<>();
|
||||
supportedCapabilities.add(EthProtocol.ETH62);
|
||||
supportedCapabilities.add(EthProtocol.ETH63);
|
||||
final GraphQLDataFetchers dataFetchers = new GraphQLDataFetchers(supportedCapabilities);
|
||||
graphQL = GraphQLProvider.buildGraphQL(dataFetchers);
|
||||
service = createGraphQLHttpService();
|
||||
service.start().join();
|
||||
// Build an OkHttp client.
|
||||
client = createHttpClientforMtls();
|
||||
baseUrl = service.url() + "/graphql/";
|
||||
}
|
||||
|
||||
public static OkHttpClient createHttpClientforMtls() throws Exception {
|
||||
|
||||
// Create a temporary truststore file
|
||||
File truststoreFile = File.createTempFile("truststore", ".jks");
|
||||
truststoreFile.deleteOnExit();
|
||||
|
||||
// Create a PKCS12 truststore and load the server's certificate
|
||||
KeyStore trustStore = KeyStore.getInstance("JKS");
|
||||
trustStore.load(null, null);
|
||||
trustStore.setCertificateEntry("alias", ssc.cert());
|
||||
|
||||
// Save the truststore to the temporary file
|
||||
try (FileOutputStream fos = new FileOutputStream(truststoreFile)) {
|
||||
trustStore.store(fos, "password".toCharArray());
|
||||
}
|
||||
|
||||
// Create TrustManagerFactory
|
||||
TrustManagerFactory trustManagerFactory =
|
||||
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init(trustStore);
|
||||
|
||||
// Get TrustManagers
|
||||
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
|
||||
// Create a temporary keystore file
|
||||
File keystoreFile = File.createTempFile("keystore", ".jks");
|
||||
keystoreFile.deleteOnExit();
|
||||
|
||||
// Create a PKCS12 keystore and load the client's certificate
|
||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||
keyStore.load(null, "password".toCharArray());
|
||||
keyStore.setKeyEntry(
|
||||
"alias",
|
||||
clientSsc.key(),
|
||||
"password".toCharArray(),
|
||||
new java.security.cert.Certificate[] {clientSsc.cert()});
|
||||
|
||||
// Save the keystore to the temporary file
|
||||
try (FileOutputStream fos = new FileOutputStream(keystoreFile)) {
|
||||
keyStore.store(fos, "password".toCharArray());
|
||||
}
|
||||
|
||||
// Create KeyManagerFactory
|
||||
KeyManagerFactory keyManagerFactory =
|
||||
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
keyManagerFactory.init(keyStore, "password".toCharArray());
|
||||
|
||||
// Get KeyManagers
|
||||
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
|
||||
|
||||
// Initialize SSLContext
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
// Obtain a SecureRandom instance
|
||||
SecureRandom secureRandom = SecureRandom.getInstanceStrong();
|
||||
|
||||
// Initialize SSLContext
|
||||
sslContext.init(keyManagers, trustManagers, secureRandom);
|
||||
|
||||
if (!(trustManagers[0] instanceof X509TrustManager)) {
|
||||
throw new IllegalStateException(
|
||||
"Unexpected default trust managers: " + Arrays.toString(trustManagers));
|
||||
}
|
||||
|
||||
// Create OkHttpClient with custom SSLSocketFactory and TrustManager
|
||||
return new OkHttpClient.Builder()
|
||||
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0])
|
||||
.hostnameVerifier((hostname, session) -> "localhost".equals(hostname))
|
||||
.followRedirects(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static GraphQLHttpService createGraphQLHttpService(final GraphQLConfiguration config)
|
||||
throws Exception {
|
||||
return new GraphQLHttpService(
|
||||
vertx, folder, config, graphQL, graphQlContextMap, Mockito.mock(EthScheduler.class));
|
||||
}
|
||||
|
||||
private static GraphQLHttpService createGraphQLHttpService() throws Exception {
|
||||
return new GraphQLHttpService(
|
||||
vertx,
|
||||
folder,
|
||||
createGraphQLConfig(),
|
||||
graphQL,
|
||||
graphQlContextMap,
|
||||
Mockito.mock(EthScheduler.class));
|
||||
}
|
||||
|
||||
private static GraphQLConfiguration createGraphQLConfig() throws Exception {
|
||||
final GraphQLConfiguration config = GraphQLConfiguration.createDefault();
|
||||
|
||||
// Create a temporary keystore file
|
||||
File keystoreFile = File.createTempFile("keystore", ".jks");
|
||||
keystoreFile.deleteOnExit();
|
||||
|
||||
// Create a PKCS12 keystore and load the self-signed certificate
|
||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||
keyStore.load(null, "password".toCharArray());
|
||||
keyStore.setKeyEntry(
|
||||
"alias",
|
||||
ssc.key(),
|
||||
"password".toCharArray(),
|
||||
new java.security.cert.Certificate[] {ssc.cert()});
|
||||
|
||||
// Save the keystore to the temporary file
|
||||
FileOutputStream fos = new FileOutputStream(keystoreFile);
|
||||
keyStore.store(fos, "password".toCharArray());
|
||||
|
||||
// Create a temporary password file
|
||||
File keystorePasswordFile = File.createTempFile("keystorePassword", ".txt");
|
||||
keystorePasswordFile.deleteOnExit();
|
||||
try (Writer writer =
|
||||
Files.newBufferedWriter(keystorePasswordFile.toPath(), Charset.defaultCharset())) {
|
||||
writer.write("password");
|
||||
}
|
||||
|
||||
// Create a temporary truststore file
|
||||
File truststoreFile = File.createTempFile("truststore", ".jks");
|
||||
truststoreFile.deleteOnExit();
|
||||
|
||||
// Create a JKS truststore and load the client's certificate
|
||||
KeyStore trustStore = KeyStore.getInstance("JKS");
|
||||
trustStore.load(null, "password".toCharArray());
|
||||
trustStore.setCertificateEntry("clientAlias", clientSsc.cert());
|
||||
|
||||
// Save the truststore to the temporary file
|
||||
try (FileOutputStream fos2 = new FileOutputStream(truststoreFile)) {
|
||||
trustStore.store(fos2, "password".toCharArray());
|
||||
}
|
||||
|
||||
config.setPort(0);
|
||||
config.setTlsEnabled(true);
|
||||
config.setTlsKeyStorePath(keystoreFile.getAbsolutePath());
|
||||
config.setTlsKeyStorePasswordFile(keystorePasswordFile.getAbsolutePath());
|
||||
config.setMtlsEnabled(true);
|
||||
config.setTlsTrustStorePath(truststoreFile.getAbsolutePath());
|
||||
config.setTlsTrustStorePasswordFile(keystorePasswordFile.getAbsolutePath());
|
||||
return config;
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setupConstants() {
|
||||
final URL blocksUrl = BlockTestUtil.getTestBlockchainUrl();
|
||||
|
||||
final URL genesisJsonUrl = BlockTestUtil.getTestGenesisUrl();
|
||||
|
||||
Assertions.assertThat(blocksUrl).isNotNull();
|
||||
Assertions.assertThat(genesisJsonUrl).isNotNull();
|
||||
}
|
||||
|
||||
/** Tears down the HTTP server. */
|
||||
@AfterAll
|
||||
public static void shutdownServer() {
|
||||
client.dispatcher().executorService().shutdown();
|
||||
client.connectionPool().evictAll();
|
||||
service.stop().join();
|
||||
vertx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidCallToStart() {
|
||||
service
|
||||
.start()
|
||||
.whenComplete(
|
||||
(unused, exception) -> assertThat(exception).isInstanceOf(IllegalStateException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void http404() throws Exception {
|
||||
try (final Response resp = client.newCall(buildGetRequest("/foo")).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(404);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleEmptyRequestAndRedirect_post() throws Exception {
|
||||
final RequestBody body = RequestBody.create("", null);
|
||||
try (final Response resp =
|
||||
client.newCall(new Request.Builder().post(body).url(service.url()).build()).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(HttpResponseStatus.PERMANENT_REDIRECT.code());
|
||||
final String location = resp.header("Location");
|
||||
Assertions.assertThat(location).isNotEmpty().isNotNull();
|
||||
final HttpUrl redirectUrl = resp.request().url().resolve(location);
|
||||
Assertions.assertThat(redirectUrl).isNotNull();
|
||||
final Request.Builder redirectBuilder = resp.request().newBuilder();
|
||||
redirectBuilder.post(resp.request().body());
|
||||
resp.body().close();
|
||||
try (final Response redirectResp =
|
||||
client.newCall(redirectBuilder.url(redirectUrl).build()).execute()) {
|
||||
Assertions.assertThat(redirectResp.code()).isEqualTo(HttpResponseStatus.BAD_REQUEST.code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleEmptyRequestAndRedirect_get() throws Exception {
|
||||
String url = service.url();
|
||||
Request req = new Request.Builder().get().url(url).build();
|
||||
try (final Response resp = client.newCall(req).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(HttpResponseStatus.PERMANENT_REDIRECT.code());
|
||||
final String location = resp.header("Location");
|
||||
Assertions.assertThat(location).isNotEmpty().isNotNull();
|
||||
final HttpUrl redirectUrl = resp.request().url().resolve(location);
|
||||
Assertions.assertThat(redirectUrl).isNotNull();
|
||||
final Request.Builder redirectBuilder = resp.request().newBuilder();
|
||||
redirectBuilder.get();
|
||||
resp.body().close();
|
||||
try (final Response redirectResp =
|
||||
client.newCall(redirectBuilder.url(redirectUrl).build()).execute()) {
|
||||
Assertions.assertThat(redirectResp.code()).isEqualTo(HttpResponseStatus.BAD_REQUEST.code());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleInvalidQuerySchema() throws Exception {
|
||||
final RequestBody body = RequestBody.create("{gasPrice1}", GRAPHQL);
|
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
|
||||
final JsonObject json = new JsonObject(resp.body().string());
|
||||
testHelper.assertValidGraphQLError(json);
|
||||
Assertions.assertThat(resp.code()).isEqualTo(400);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void query_get() throws Exception {
|
||||
final Wei price = Wei.of(16);
|
||||
Mockito.when(blockchainQueries.gasPrice()).thenReturn(price);
|
||||
Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price);
|
||||
|
||||
try (final Response resp = client.newCall(buildGetRequest("?query={gasPrice}")).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(200);
|
||||
final JsonObject json = new JsonObject(resp.body().string());
|
||||
testHelper.assertValidGraphQLResult(json);
|
||||
final String result = json.getJsonObject("data").getString("gasPrice");
|
||||
Assertions.assertThat(result).isEqualTo("0x10");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void query_jsonPost() throws Exception {
|
||||
final RequestBody body = RequestBody.create("{\"query\":\"{gasPrice}\"}", JSON);
|
||||
final Wei price = Wei.of(16);
|
||||
Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price);
|
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(200); // Check general format of result
|
||||
final JsonObject json = new JsonObject(resp.body().string());
|
||||
testHelper.assertValidGraphQLResult(json);
|
||||
final String result = json.getJsonObject("data").getString("gasPrice");
|
||||
Assertions.assertThat(result).isEqualTo("0x10");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void query_graphqlPost() throws Exception {
|
||||
final RequestBody body = RequestBody.create("{gasPrice}", GRAPHQL);
|
||||
final Wei price = Wei.of(16);
|
||||
Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price);
|
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(200); // Check general format of result
|
||||
final JsonObject json = new JsonObject(resp.body().string());
|
||||
testHelper.assertValidGraphQLResult(json);
|
||||
final String result = json.getJsonObject("data").getString("gasPrice");
|
||||
Assertions.assertThat(result).isEqualTo("0x10");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void query_untypedPost() throws Exception {
|
||||
final RequestBody body = RequestBody.create("{gasPrice}", null);
|
||||
final Wei price = Wei.of(16);
|
||||
Mockito.when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price);
|
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(200); // Check general format of result
|
||||
final JsonObject json = new JsonObject(resp.body().string());
|
||||
testHelper.assertValidGraphQLResult(json);
|
||||
final String result = json.getJsonObject("data").getString("gasPrice");
|
||||
Assertions.assertThat(result).isEqualTo("0x10");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSocketAddressWhenActive() {
|
||||
final InetSocketAddress socketAddress = service.socketAddress();
|
||||
Assertions.assertThat(socketAddress.getAddress().getHostAddress()).isEqualTo("127.0.0.1");
|
||||
Assertions.assertThat(socketAddress.getPort()).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSocketAddressWhenStoppedIsEmpty() throws Exception {
|
||||
final GraphQLHttpService service = createGraphQLHttpService();
|
||||
|
||||
final InetSocketAddress socketAddress = service.socketAddress();
|
||||
Assertions.assertThat(socketAddress.getAddress().getHostAddress()).isEqualTo("0.0.0.0");
|
||||
Assertions.assertThat(socketAddress.getPort()).isZero();
|
||||
Assertions.assertThat(service.url()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSocketAddressWhenBindingToAllInterfaces() throws Exception {
|
||||
final GraphQLConfiguration config = createGraphQLConfig();
|
||||
config.setHost("0.0.0.0");
|
||||
final GraphQLHttpService service = createGraphQLHttpService(config);
|
||||
service.start().join();
|
||||
|
||||
try {
|
||||
final InetSocketAddress socketAddress = service.socketAddress();
|
||||
Assertions.assertThat(socketAddress.getAddress().getHostAddress()).isEqualTo("0.0.0.0");
|
||||
Assertions.assertThat(socketAddress.getPort()).isPositive();
|
||||
Assertions.assertThat(!service.url().contains("0.0.0.0")).isTrue();
|
||||
} finally {
|
||||
service.stop().join();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseContainsJsonContentTypeHeader() throws Exception {
|
||||
|
||||
final RequestBody body = RequestBody.create("{gasPrice}", GRAPHQL);
|
||||
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
|
||||
Assertions.assertThat(resp.header("Content-Type")).isEqualTo(JSON.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ethGetBlockNumberByBlockHash() throws Exception {
|
||||
final long blockNumber = 12345L;
|
||||
final Hash blockHash = Hash.hash(Bytes.of(1));
|
||||
@SuppressWarnings("unchecked")
|
||||
final BlockWithMetadata<TransactionWithMetadata, Hash> block =
|
||||
Mockito.mock(BlockWithMetadata.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
final BlockHeader blockHeader = Mockito.mock(BlockHeader.class);
|
||||
|
||||
Mockito.when(blockchainQueries.blockByHash(blockHash)).thenReturn(Optional.of(block));
|
||||
Mockito.when(block.getHeader()).thenReturn(blockHeader);
|
||||
Mockito.when(blockHeader.getNumber()).thenReturn(blockNumber);
|
||||
|
||||
final String query = "{block(hash:\"" + blockHash + "\") {number}}";
|
||||
|
||||
final RequestBody body = RequestBody.create(query, GRAPHQL);
|
||||
try (final Response resp = client.newCall(buildPostRequest(body)).execute()) {
|
||||
Assertions.assertThat(resp.code()).isEqualTo(200);
|
||||
final String jsonStr = resp.body().string();
|
||||
final JsonObject json = new JsonObject(jsonStr);
|
||||
testHelper.assertValidGraphQLResult(json);
|
||||
final String result = json.getJsonObject("data").getJsonObject("block").getString("number");
|
||||
Assertions.assertThat(Integer.parseInt(result.substring(2), 16)).isEqualTo(blockNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private Request buildPostRequest(final RequestBody body) {
|
||||
return new Request.Builder().post(body).url(baseUrl).build();
|
||||
}
|
||||
|
||||
private Request buildGetRequest(final String path) {
|
||||
return new Request.Builder().get().url(baseUrl + path).build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user