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:
Bhanu Pulluri
2025-02-11 21:28:09 -05:00
committed by GitHub
parent 3638944a6c
commit f3bbb90cb0
6 changed files with 723 additions and 10 deletions

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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();
}
}