From 4c4fe0c7c9af5b88c86bbd547b35fc0fe47ada22 Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Fri, 22 Nov 2024 12:50:59 -0500 Subject: [PATCH 1/2] Add TLS/mTLS options and configure the GraphQL HTTP service Signed-off-by: Bhanu Pulluri --- .../besu/cli/options/GraphQlOptions.java | 60 +- .../src/test/resources/everything_config.toml | 6 + .../api/graphql/GraphQLConfiguration.java | 62 ++ .../api/graphql/GraphQLHttpService.java | 49 +- .../api/graphql/GraphQLHttpsServiceTest.java | 545 ++++++++++++++++++ 5 files changed, 713 insertions(+), 9 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java index 643ca22e005..e72e6c23d39 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/GraphQlOptions.java @@ -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 void validate(final Logger logger, final CommandLine commandLine) { 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 GraphQLConfiguration graphQLConfiguration( 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; } diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index 960b2b5772c..79d467b484f 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -108,6 +108,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 diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java index a2829edb071..4218e35b185 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLConfiguration.java @@ -18,6 +18,9 @@ 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 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,58 @@ public void setHttpTimeoutSec(final long httpTimeoutSec) { this.httpTimeoutSec = httpTimeoutSec; } + public String getTlsKeyStorePath() { + return tlsKeyStorePath; + } + + public void setTlsKeyStorePath(final String tlsKeyStorePath) { + this.tlsKeyStorePath = tlsKeyStorePath; + } + + public String getTlsKeyStorePassword() throws Exception { + return new String( + Files.readAllBytes(Paths.get(tlsKeyStorePasswordFile)), Charset.defaultCharset()) + .trim(); + } + + public void setTlsKeyStorePasswordFile(final String tlsKeyStorePasswordFile) { + this.tlsKeyStorePasswordFile = tlsKeyStorePasswordFile; + } + + public String getTlsTrustStorePath() { + return tlsTrustStorePath; + } + + public void setTlsTrustStorePath(final String tlsTrustStorePath) { + this.tlsTrustStorePath = tlsTrustStorePath; + } + + public String getTlsTrustStorePassword() throws Exception { + return new String( + Files.readAllBytes(Paths.get(tlsTrustStorePasswordFile)), Charset.defaultCharset()) + .trim(); + } + + public void setTlsTrustStorePasswordFile(final String tlsTrustStorePasswordFile) { + this.tlsTrustStorePasswordFile = tlsTrustStorePasswordFile; + } + + public boolean isTlsEnabled() { + return tlsEnabled; + } + + public void setTlsEnabled(final boolean tlsEnabled) { + this.tlsEnabled = tlsEnabled; + } + + public boolean isMtlsEnabled() { + return mtlsEnabled; + } + + public void setMtlsEnabled(final boolean mtlsEnabled) { + this.mtlsEnabled = mtlsEnabled; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java index fc763a1e3ba..f494cfc54c5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpService.java @@ -51,6 +51,7 @@ 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.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 @@ private void validateConfig(final GraphQLConfiguration config) { 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 String url() { 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 diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java new file mode 100644 index 00000000000..274636445ff --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLHttpsServiceTest.java @@ -0,0 +1,545 @@ +/* + * Copyright ConsenSys AG. + * + * 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.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.List; +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.ArgumentMatchers; +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 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 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 ethGetUncleCountByBlockHash() throws Exception { + final int uncleCount = 4; + final Hash blockHash = Hash.hash(Bytes.of(1)); + @SuppressWarnings("unchecked") + final BlockWithMetadata block = + Mockito.mock(BlockWithMetadata.class); + @SuppressWarnings("unchecked") + final List list = Mockito.mock(List.class); + + Mockito.when(blockchainQueries.blockByHash(blockHash)).thenReturn(Optional.of(block)); + Mockito.when(block.getOmmers()).thenReturn(list); + Mockito.when(list.size()).thenReturn(uncleCount); + + final String query = "{block(hash:\"" + blockHash + "\") {ommerCount}}"; + + 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("ommerCount"); + Assertions.assertThat(Bytes.fromHexStringLenient(result).toInt()).isEqualTo(uncleCount); + } + } + + @Test + public void ethGetUncleCountByBlockNumber() throws Exception { + final int uncleCount = 5; + @SuppressWarnings("unchecked") + final BlockWithMetadata block = + Mockito.mock(BlockWithMetadata.class); + @SuppressWarnings("unchecked") + final List list = Mockito.mock(List.class); + Mockito.when(blockchainQueries.blockByNumber(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(block)); + Mockito.when(block.getOmmers()).thenReturn(list); + Mockito.when(list.size()).thenReturn(uncleCount); + + final String query = "{block(number:\"3\") {ommerCount}}"; + + 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("ommerCount"); + Assertions.assertThat(Bytes.fromHexStringLenient(result).toInt()).isEqualTo(uncleCount); + } + } + + @Test + public void ethGetUncleCountByBlockLatest() throws Exception { + final int uncleCount = 5; + @SuppressWarnings("unchecked") + final BlockWithMetadata block = + Mockito.mock(BlockWithMetadata.class); + @SuppressWarnings("unchecked") + final List list = Mockito.mock(List.class); + Mockito.when(blockchainQueries.latestBlock()).thenReturn(Optional.of(block)); + Mockito.when(block.getOmmers()).thenReturn(list); + Mockito.when(list.size()).thenReturn(uncleCount); + + final String query = "{block {ommerCount}}"; + + 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("ommerCount"); + Assertions.assertThat(Bytes.fromHexStringLenient(result).toInt()).isEqualTo(uncleCount); + } + } + + 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(); + } +} From 4bab86e39bf0e975636e35c218ceb6fb9246d996 Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri Date: Fri, 22 Nov 2024 13:00:37 -0500 Subject: [PATCH 2/2] update changelog Signed-off-by: Bhanu Pulluri --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a53280feb..21c69180038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Update Java dependencies [#7786](https://github.com/hyperledger/besu/pull/7786) - Add a method to get all the transaction in the pool, to the `TransactionPoolService`, to easily access the transaction pool content from plugins [#7813](https://github.com/hyperledger/besu/pull/7813) - Add a method to check if a metric category is enabled to the plugin API [#7832](https://github.com/hyperledger/besu/pull/7832) +- Add TLS/mTLS options and configure the GraphQL HTTP service[#7910](https://github.com/hyperledger/besu/pull/7910) ### Bug fixes - Fix registering new metric categories from plugins [#7825](https://github.com/hyperledger/besu/pull/7825)