/* * Copyright 2019 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. */ package tech.pegasys.ethsigner.jsonrpcproxy; import static io.restassured.RestAssured.given; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import static org.hamcrest.core.IsEqual.equalTo; import static org.mockserver.integration.ClientAndServer.startClientAndServer; import static org.mockserver.matchers.Times.exactly; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; import static org.mockserver.model.JsonBody.json; import static org.web3j.utils.Async.defaultExecutorService; import tech.pegasys.ethsigner.core.Runner; import tech.pegasys.ethsigner.core.jsonrpc.JsonDecoder; import tech.pegasys.ethsigner.core.requesthandler.sendtransaction.DownstreamPathCalculator; import tech.pegasys.ethsigner.jsonrpcproxy.model.request.EthNodeRequest; import tech.pegasys.ethsigner.jsonrpcproxy.model.request.EthRequestFactory; import tech.pegasys.ethsigner.jsonrpcproxy.model.request.EthSignerRequest; import tech.pegasys.ethsigner.jsonrpcproxy.model.response.EthNodeResponse; import tech.pegasys.ethsigner.jsonrpcproxy.model.response.EthResponseFactory; import tech.pegasys.ethsigner.jsonrpcproxy.model.response.EthSignerResponse; import tech.pegasys.signers.secp256k1.api.SingleTransactionSignerProvider; import tech.pegasys.signers.secp256k1.api.TransactionSigner; import tech.pegasys.signers.secp256k1.api.TransactionSignerProvider; import tech.pegasys.signers.secp256k1.filebased.FileBasedSignerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.Resources; import io.restassured.RestAssured; import io.vertx.core.Vertx; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpServerOptions; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; import org.mockserver.integration.ClientAndServer; import org.mockserver.model.Header; import org.mockserver.model.JsonBody; import org.mockserver.model.RegexBody; import org.web3j.crypto.CipherException; import org.web3j.crypto.Credentials; import org.web3j.crypto.WalletUtils; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.JsonRpc2_0Web3j; import org.web3j.protocol.eea.Eea; import org.web3j.protocol.eea.JsonRpc2_0Eea; public class IntegrationTestBase { private static final Logger LOG = LogManager.getLogger(); private static final String PORTS_FILENAME = "ethsigner.ports"; private static final String HTTP_JSON_RPC_KEY = "http-jsonrpc"; private static final String LOCALHOST = "127.0.0.1"; public static final long DEFAULT_CHAIN_ID = 9; public static final int DEFAULT_ID = 77; static final String MALFORMED_JSON = "{Bad Json: {{{}"; private static Vertx vertx; private static Runner runner; static ClientAndServer clientAndServer; static Credentials credentials; private JsonRpc2_0Web3j jsonRpc; private JsonRpc2_0Eea eeaJsonRpc; protected final EthRequestFactory request = new EthRequestFactory(); protected final EthResponseFactory response = new EthResponseFactory(); static String unlockedAccount; private static final Duration downstreamTimeout = Duration.ofSeconds(1); @TempDir static Path dataPath; static void setupEthSigner(final long chainId) throws IOException, CipherException { setupEthSigner(chainId, ""); } static void setupEthSigner(final long chainId, final String downstreamHttpRequestPath) throws IOException, CipherException { clientAndServer = startClientAndServer(); final File keyFile = createKeyFile(); final File passwordFile = createFile("password"); credentials = WalletUtils.loadCredentials("password", keyFile); final TransactionSignerProvider transactionSignerProvider = new SingleTransactionSignerProvider(transactionSigner(keyFile, passwordFile)); final HttpClientOptions httpClientOptions = new HttpClientOptions(); httpClientOptions.setDefaultHost(LOCALHOST); httpClientOptions.setDefaultPort(clientAndServer.getLocalPort()); final HttpServerOptions httpServerOptions = new HttpServerOptions(); httpServerOptions.setPort(0); httpServerOptions.setHost("localhost"); // Force TransactionDeserialisation to fail final ObjectMapper jsonObjectMapper = new ObjectMapper(); jsonObjectMapper.configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true); jsonObjectMapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true); final JsonDecoder jsonDecoder = new JsonDecoder(jsonObjectMapper); vertx = Vertx.vertx(); runner = new Runner( chainId, transactionSignerProvider, httpClientOptions, httpServerOptions, downstreamTimeout, new DownstreamPathCalculator(downstreamHttpRequestPath), jsonDecoder, dataPath, vertx, singletonList("sample.com")); runner.start(); final Path portsFile = dataPath.resolve(PORTS_FILENAME); waitForNonEmptyFileToExist(portsFile); final int ethSignerPort = httpJsonRpcPort(portsFile); RestAssured.port = ethSignerPort; LOG.info( "Started ethSigner on port {}, eth stub node on port {}", ethSignerPort, clientAndServer.getLocalPort()); unlockedAccount = transactionSignerProvider.availableAddresses().stream().findAny().orElseThrow(); } Web3j jsonRpc() { return jsonRpc; } Eea eeaJsonRpc() { return eeaJsonRpc; } @BeforeEach public void setup() { jsonRpc = new JsonRpc2_0Web3j(null, 2000, defaultExecutorService()); eeaJsonRpc = new JsonRpc2_0Eea(null); if (clientAndServer.isRunning()) { clientAndServer.reset(); } } @AfterAll public static void teardown() { clientAndServer.stop(); vertx.close(); clientAndServer = null; runner = null; } void setUpEthNodeResponse(final EthNodeRequest request, final EthNodeResponse response) { final List<Header> headers = convertHeadersToMockServerHeaders(response.getHeaders()); clientAndServer .when(request().withBody(json(request.getBody())), exactly(1)) .respond( response() .withBody(response.getBody()) .withHeaders(headers) .withStatusCode(response.getStatusCode())); } void setupEthNodeResponse( final String bodyRegex, final EthNodeResponse response, final int count) { final List<Header> headers = convertHeadersToMockServerHeaders(response.getHeaders()); clientAndServer .when(request().withBody(new RegexBody(bodyRegex)), exactly(count)) .respond( response() .withBody(response.getBody()) .withHeaders(headers) .withStatusCode(response.getStatusCode())); } void timeoutRequest(final String bodyRegex) { final int ENSURE_TIMEOUT = 5; clientAndServer .when(request().withBody(new RegexBody(bodyRegex))) .respond( response() .withDelay(TimeUnit.MILLISECONDS, downstreamTimeout.toMillis() + ENSURE_TIMEOUT)); } void timeoutRequest(final EthNodeRequest request) { final int ENSURE_TIMEOUT = 5; clientAndServer .when(request().withBody(json(request.getBody())), exactly(1)) .respond( response() .withDelay(TimeUnit.MILLISECONDS, downstreamTimeout.toMillis() + ENSURE_TIMEOUT)); } void sendPostRequestAndVerifyResponse( final EthSignerRequest request, final EthSignerResponse expectResponse) { sendPostRequestAndVerifyResponse(request, expectResponse, "/"); } void sendPostRequestAndVerifyResponse( final EthSignerRequest request, final EthSignerResponse expectResponse, final String path) { given() .when() .body(request.getBody()) .headers(request.getHeaders()) .post(path) .then() .statusCode(expectResponse.getStatusCode()) .body(equalTo(expectResponse.getBody())) .headers(expectResponse.getHeaders()); } void sendPutRequestAndVerifyResponse( final EthSignerRequest request, final EthSignerResponse expectResponse, final String path) { given() .when() .body(request.getBody()) .headers(request.getHeaders()) .put(path) .then() .statusCode(expectResponse.getStatusCode()) .body(equalTo(expectResponse.getBody())) .headers(expectResponse.getHeaders()); } void sendGetRequestAndVerifyResponse( final EthSignerRequest request, final EthSignerResponse expectResponse, final String path) { given() .when() .body(request.getBody()) .headers(request.getHeaders()) .get(path) .then() .statusCode(expectResponse.getStatusCode()) .body(equalTo(expectResponse.getBody())) .headers(expectResponse.getHeaders()); } void sendDeleteRequestAndVerifyResponse( final EthSignerRequest request, final EthSignerResponse expectResponse, final String path) { given() .when() .body(request.getBody()) .headers(request.getHeaders()) .delete(path) .then() .statusCode(expectResponse.getStatusCode()) .body(equalTo(expectResponse.getBody())) .headers(expectResponse.getHeaders()); } void verifyEthNodeReceived(final String proxyBodyRequest) { clientAndServer.verify( request() .withBody(JsonBody.json(proxyBodyRequest)) .withHeaders(convertHeadersToMockServerHeaders(emptyMap()))); } void verifyEthNodeReceived(final Map<String, String> headers, final String proxyBodyRequest) { clientAndServer.verify( request() .withBody(proxyBodyRequest) .withHeaders(convertHeadersToMockServerHeaders(headers))); } void verifyEthNodeReceived( final Map<String, String> headers, final String proxyBodyRequest, final String path) { clientAndServer.verify( request() .withPath(path) .withBody(JsonBody.json(proxyBodyRequest)) .withHeaders(convertHeadersToMockServerHeaders(headers))); } private List<Header> convertHeadersToMockServerHeaders(final Map<String, String> headers) { return headers.entrySet().stream() .map((Map.Entry<String, String> e) -> new Header(e.getKey(), e.getValue())) .collect(toList()); } private static TransactionSigner transactionSigner(final File keyFile, final File passwordFile) { return FileBasedSignerFactory.createSigner(keyFile.toPath(), passwordFile.toPath()); } @SuppressWarnings("UnstableApiUsage") private static File createKeyFile() throws IOException { final URL walletResource = Resources.getResource("keyfile.json"); final Path wallet = Files.createTempFile("ethsigner_intg_keyfile", ".json"); Files.write(wallet, Resources.toString(walletResource, UTF_8).getBytes(UTF_8)); final File keyFile = wallet.toFile(); keyFile.deleteOnExit(); return keyFile; } private static File createFile(final String s) throws IOException { final Path path = Files.createTempFile("file", ".file"); Files.write(path, s.getBytes(UTF_8)); final File file = path.toFile(); file.deleteOnExit(); return file; } private static int httpJsonRpcPort(final Path portsFile) { try (final FileInputStream fis = new FileInputStream(portsFile.toString())) { final Properties portProperties = new Properties(); portProperties.load(fis); final String value = portProperties.getProperty(HTTP_JSON_RPC_KEY); return Integer.parseInt(value); } catch (final IOException e) { throw new RuntimeException("Error reading Web3Provider ports file", e); } } private static void waitForNonEmptyFileToExist(final Path path) { final File file = path.toFile(); Awaitility.waitAtMost(30, TimeUnit.SECONDS) .until( () -> { if (file.exists()) { try (final Stream<String> s = Files.lines(file.toPath())) { return s.count() > 0; } } return false; }); } }