Skip to content

Commit

Permalink
Reverse proxy and send transaction signing (Consensys#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
jframe authored Mar 7, 2019
1 parent 0c46764 commit 33ef337
Show file tree
Hide file tree
Showing 15 changed files with 642 additions and 98 deletions.
4 changes: 4 additions & 0 deletions ethfirewall/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ dependencies {

implementation 'org.apache.logging.log4j:log4j-api'
implementation 'org.apache.logging.log4j:log4j-core'
implementation 'org.apache.logging.log4j:log4j-slf4j-impl'

implementation 'org.web3j:core'
implementation 'org.web3j:crypto'

implementation 'io.vertx:vertx-core'
implementation 'io.vertx:vertx-web'
implementation 'io.vertx:vertx-web-client'

testImplementation 'junit:junit'
testImplementation 'org.assertj:assertj-core'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import picocli.CommandLine.RunLast;

public final class EthFirewall {
private static final int SUCCESS_EXIT_CODE = 0;
private static final int ERROR_EXIT_CODE = 1;

public static void main(final String... args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@
*/
package tech.pegasys.ethfirewall;

import tech.pegasys.ethfirewall.jsonrpcproxy.JsonRpcHttpService;
import tech.pegasys.ethfirewall.jsonrpcproxy.TransactionSigner;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.function.Supplier;

import com.google.common.base.Suppliers;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.ext.web.client.WebClientOptions;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.web3j.crypto.CipherException;
import picocli.CommandLine;
import picocli.CommandLine.AbstractParseResultHandler;
import picocli.CommandLine.Command;
Expand All @@ -42,8 +49,7 @@
footerHeading = "%n",
footer = "Ethfirewall is licensed under the Apache License 2.0")
public class EthFirewallCommand implements Runnable {

private static final Logger LOG = LogManager.getLogger();
private static final Logger LOG = LoggerFactory.getLogger(JsonRpcHttpService.class);
private CommandLine commandLine;

private final Supplier<EthFirewallExceptionHandler> exceptionHandlerSupplier =
Expand Down Expand Up @@ -115,7 +121,28 @@ public void run() {
System.out.println("Setting logging level to " + logLevel.name());
Configurator.setAllLevels("", logLevel);

// Create TransactionSigner, ReverseProxy and http request forwarders.
try {
final TransactionSigner transactionSigner =
TransactionSigner.createFrom(keyFilename, password);
final WebClientOptions clientOptions =
new WebClientOptions()
.setDefaultPort(downstreamHttpPort)
.setDefaultHost(downstreamHttpHost);
final HttpServerOptions serverOptions =
new HttpServerOptions()
.setPort(httpListenPort)
.setHost(httpListenHost)
.setReuseAddress(true)
.setReusePort(true);

final Runner runner = new Runner(transactionSigner, clientOptions, serverOptions);
runner.start();
} catch (IOException ex) {
LOG.info(
"Unable to access supplied keyfile, or file does not conform to V3 keystore standard.");
} catch (CipherException ex) {
LOG.info("Unable to decode keyfile with supplied password.");
}
}

public EthFirewallExceptionHandler exceptionHandler() {
Expand Down
81 changes: 81 additions & 0 deletions ethfirewall/src/main/java/tech/pegasys/ethfirewall/Runner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.ethfirewall;

import tech.pegasys.ethfirewall.jsonrpcproxy.JsonRpcHttpService;
import tech.pegasys.ethfirewall.jsonrpcproxy.PassThroughHandler;
import tech.pegasys.ethfirewall.jsonrpcproxy.RequestMapper;
import tech.pegasys.ethfirewall.jsonrpcproxy.TransactionBodyProvider;
import tech.pegasys.ethfirewall.jsonrpcproxy.TransactionSigner;

import io.vertx.core.AsyncResult;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.ext.web.RoutingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Runner {

private static final Logger LOG = LoggerFactory.getLogger(Runner.class);
private TransactionSigner transactionSigner;
private HttpClientOptions clientOptions;
private HttpServerOptions serverOptions;

public Runner(
final TransactionSigner transactionSigner,
final HttpClientOptions clientOptions,
final HttpServerOptions serverOptions) {
this.transactionSigner = transactionSigner;
this.clientOptions = clientOptions;
this.serverOptions = serverOptions;
}

public void start() {
// NOTE: Starting vertx spawns daemon threads, meaning the app may complete, but not terminate.
final Vertx vertx = Vertx.vertx();
final RequestMapper requestMapper = createRequestMapper(vertx, transactionSigner);
final JsonRpcHttpService httpService = new JsonRpcHttpService(serverOptions, requestMapper);

vertx.deployVerticle(httpService, this::handleDeployResult);
}

private RequestMapper createRequestMapper(
final Vertx vertx, final TransactionSigner transactionSigner) {

final HttpClient downStreamConnection = vertx.createHttpClient(clientOptions);
final PassThroughHandler passThroughHandler =
new PassThroughHandler(downStreamConnection, RoutingContext::getBody);

final RequestMapper requestMapper = new RequestMapper(passThroughHandler);

final TransactionBodyProvider sendTransactionHandler =
new TransactionBodyProvider(transactionSigner);

requestMapper.addHandler(
"eth_sendTransaction",
new PassThroughHandler(downStreamConnection, sendTransactionHandler));

return requestMapper;
}

private void handleDeployResult(final AsyncResult<?> result) {
if (result.succeeded()) {
LOG.info("Deployment id is: {}", result.result());
} else {
LOG.warn("Deployment failed! ", result.cause());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.ethfirewall.jsonrpcproxy;

import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.RoutingContext;

@FunctionalInterface
public interface BodyProvider {
Buffer getBody(RoutingContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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.ethfirewall.jsonrpcproxy;

import io.netty.handler.codec.http.HttpHeaderValues;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.ResponseContentTypeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JsonRpcHttpService extends AbstractVerticle {

private static final Logger LOG = LoggerFactory.getLogger(JsonRpcHttpService.class);
private static final String JSON = HttpHeaderValues.APPLICATION_JSON.toString();

private final RequestMapper requestHandlerMapper;
private final HttpServerOptions serverOptions;
private HttpServer httpServer = null;

public JsonRpcHttpService(
final HttpServerOptions serverOptions, final RequestMapper requestHandlerMapper) {
this.serverOptions = serverOptions;
this.requestHandlerMapper = requestHandlerMapper;
}

@Override
public void start(final Future<Void> startFuture) {
httpServer = vertx.createHttpServer(serverOptions);
httpServer
.requestHandler(router())
.listen(
result -> {
if (result.succeeded()) {
LOG.info("Json RPC server started on {}", httpServer.actualPort());
startFuture.complete();
} else {
LOG.error("Json RPC server failed to listen", result.cause());
startFuture.fail(result.cause());
}
});
}

@Override
public void stop(final Future<Void> stopFuture) {
httpServer.close(
result -> {
if (result.succeeded()) {
stopFuture.complete();
} else {
stopFuture.fail(result.cause());
}
});
}

private Router router() {
final Router router = Router.router(vertx);
router
.route(HttpMethod.POST, "/")
.produces(JSON)
.handler(BodyHandler.create())
.handler(ResponseContentTypeHandler.create())
.failureHandler(new LogErrorHandler())
.handler(this::handleJsonRpc);
router.route().handler(context -> {});
return router;
}

private void handleJsonRpc(final RoutingContext routingContext) {
final Handler<RoutingContext> handler =
requestHandlerMapper.getMatchingHandler(routingContext.getBodyAsJson());
handler.handle(routingContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.ethfirewall.jsonrpcproxy;

import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Failure handler that records log details of the problem. */
public class LogErrorHandler implements Handler<RoutingContext> {
private static final Logger LOG = LoggerFactory.getLogger(LogErrorHandler.class);

@Override
public void handle(final RoutingContext failureContext) {

if (failureContext.failed()) {
LOG.error(
String.format("Failed sendRequest: %s", failureContext.request().absoluteURI()),
failureContext.failure());
} else {
LOG.warn("Error handler triggered without any propagated failure");
}
}
}
Loading

0 comments on commit 33ef337

Please sign in to comment.