problem: no response on invalid request to proxy

rel: #43
This commit is contained in:
Igor Artamonov
2020-08-09 18:42:23 -04:00
parent 8bd13bdb0f
commit 2abe3023b6
13 changed files with 7148 additions and 26 deletions

View File

@@ -28,7 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class TlsSetup( open class TlsSetup(
@Autowired val fileResolver: FileResolver @Autowired val fileResolver: FileResolver
) { ) {

View File

@@ -21,7 +21,7 @@ import io.emeraldpay.grpc.Chain
/** /**
* Configure HTTP Proxy to Upstreams * Configure HTTP Proxy to Upstreams
*/ */
class ProxyConfig { open class ProxyConfig {
companion object { companion object {
public const val CONFIG_ID = "parsed.proxy" public const val CONFIG_ID = "parsed.proxy"

View File

@@ -18,14 +18,19 @@ package io.emeraldpay.dshackle.proxy
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.TlsSetup import io.emeraldpay.dshackle.TlsSetup
import io.emeraldpay.dshackle.config.ProxyConfig import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.rpc.NativeCall import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.RpcException
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SslContextBuilder
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.netty.DisposableServer import reactor.netty.DisposableServer
import reactor.netty.http.server.HttpServer import reactor.netty.http.server.HttpServer
@@ -90,15 +95,27 @@ class ProxyServer(
} }
} }
fun processRequest(chain: Common.ChainRef, request: Mono<ByteArray>): Flux<ByteBuf> {
return request.map(readRpcJson)
.flatMapMany { call -> execute(chain, call) }
.onErrorResume(RpcException::class.java) { err ->
val id = err.details?.let {
if (it is JsonRpcResponse.Id) it else JsonRpcResponse.IntId(-1)
} ?: JsonRpcResponse.IntId(-1)
val json = JsonRpcResponse.error(err.code, err.rpcMessage, id)
Mono.just(Global.objectMapper.writeValueAsString(json))
}
.map { Unpooled.wrappedBuffer(it.toByteArray()) }
}
fun proxy(routeConfig: ProxyConfig.Route): BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> { fun proxy(routeConfig: ProxyConfig.Route): BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> {
val chain = Common.ChainRef.forNumber(routeConfig.blockchain.id) val chain = Common.ChainRef.forNumber(routeConfig.blockchain.id)
return BiFunction { req, resp -> return BiFunction { req, resp ->
val results = req.receive() val request = req.receive()
.aggregate() .aggregate()
.asByteArray() .asByteArray()
.map(readRpcJson) val results = processRequest(chain, request)
.flatMapMany { call -> execute(chain, call) }
.map { Unpooled.wrappedBuffer(it.toByteArray()) }
resp.addHeader(HttpHeaders.CONTENT_TYPE, "application/json") resp.addHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.send(results) .send(results)
} }

View File

@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.RpcResponseError
import io.infinitape.etherjar.rpc.json.RequestJson import io.infinitape.etherjar.rpc.json.RequestJson
@@ -36,8 +37,7 @@ import java.util.stream.Collectors
* Reader for JSON RPC request * Reader for JSON RPC request
*/ */
@Service @Service
open class ReadRpcJson( open class ReadRpcJson() : Function<ByteArray, ProxyCall> {
) : Function<ByteArray, ProxyCall> {
companion object { companion object {
private val log = LoggerFactory.getLogger(ReadRpcJson::class.java) private val log = LoggerFactory.getLogger(ReadRpcJson::class.java)
@@ -49,18 +49,21 @@ open class ReadRpcJson(
init { init {
jsonExtractor = Function { json -> jsonExtractor = Function { json ->
if ("2.0" != json["jsonrpc"]) {
throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Unsupported JSON RPC version")
}
if (json["id"] == null) { if (json["id"] == null) {
throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "ID not set") throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "ID is not set")
} }
val id = json["id"] val id = json["id"]
if ("2.0" != json["jsonrpc"]) {
if (json["jsonrpc"] == null) {
throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "jsonrpc version is not set", id?.let { JsonRpcResponse.Id.from(it) })
}
throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Unsupported JSON RPC version: " + json["jsonrpc"].toString(), id?.let { JsonRpcResponse.Id.from(it) })
}
if (!(json["method"] != null && json["method"] is String)) { if (!(json["method"] != null && json["method"] is String)) {
throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "ID not set") throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Method is not set", id?.let { JsonRpcResponse.Id.from(it) })
} }
if (json.containsKey("params") && json["params"] !is List<*>) { if (json.containsKey("params") && json["params"] !is List<*>) {
throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Params must be an array") throw RpcException(RpcResponseError.CODE_INVALID_REQUEST, "Params must be an array", id?.let { JsonRpcResponse.Id.from(it) })
} }
RequestJson<Any>( RequestJson<Any>(
json["method"].toString(), json["method"].toString(),

View File

@@ -164,6 +164,19 @@ class JsonRpcResponse(
override fun isInt(): Boolean { override fun isInt(): Boolean {
return true return true
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is IntId) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id
}
} }
class StringId(val id: String) : Id { class StringId(val id: String) : Id {
@@ -178,6 +191,20 @@ class JsonRpcResponse(
override fun isInt(): Boolean { override fun isInt(): Boolean {
return false return false
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is StringId) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
} }
class ResponseJsonSerializer : JsonSerializer<JsonRpcResponse>() { class ResponseJsonSerializer : JsonSerializer<JsonRpcResponse>() {

View File

@@ -22,7 +22,10 @@ import io.emeraldpay.dshackle.TlsSetup
import io.emeraldpay.dshackle.config.ProxyConfig import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.rpc.NativeCall import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.RpcException
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier import reactor.test.StepVerifier
import spock.lang.Specification import spock.lang.Specification
@@ -65,4 +68,24 @@ class ProxyServerSpec extends Specification {
.expectComplete() .expectComplete()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
def "Return error on invalid request"() {
setup:
ReadRpcJson read = Mock(ReadRpcJson) {
1 * apply(_) >> { throw new RpcException(-32123, "test", new JsonRpcResponse.IntId(4)) }
}
def server = new ProxyServer(
Stub(ProxyConfig),
read,
Stub(WriteRpcJson), Stub(NativeCall), Stub(TlsSetup)
)
when:
def act = server.processRequest(Common.ChainRef.CHAIN_ETHEREUM, Mono.just("".bytes))
.map { new String(it.array()) }
then:
StepVerifier.create(act)
.expectNext('{"jsonrpc":"2.0","id":4,"error":{"code":-32123,"message":"test"}}')
.expectComplete()
.verify(Duration.ofSeconds(1))
}
} }

View File

@@ -17,6 +17,7 @@
package io.emeraldpay.dshackle.proxy package io.emeraldpay.dshackle.proxy
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.RpcException import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification import spock.lang.Specification
@@ -174,4 +175,53 @@ class ReadRpcJsonSpec extends Specification {
payload.toStringUtf8() == '[143,false]' payload.toStringUtf8() == '[143,false]'
} }
} }
def "Error if id is not set"() {
when:
reader.apply('{"jsonrpc":"2.0", "method":"net_peerCount", "params":[]}'.bytes)
then:
def t = thrown(RpcException)
t.code == -32600
t.rpcMessage.toLowerCase() == "id is not set"
}
def "Error if jsonrpc is not set"() {
when:
reader.apply('{"id":2, "method":"net_peerCount", "params":[]}'.bytes)
then:
def t = thrown(RpcException)
t.code == -32600
t.rpcMessage.toLowerCase() == "jsonrpc version is not set"
t.details == new JsonRpcResponse.IntId(2)
}
def "Error if jsonrpc version is invalid"() {
when:
reader.apply('{"id":2, "jsonrpc":"3.0", "method":"net_peerCount", "params":[]}'.bytes)
then:
def t = thrown(RpcException)
t.code == -32600
t.rpcMessage.toLowerCase() == "unsupported json rpc version: 3.0"
t.details == new JsonRpcResponse.IntId(2)
}
def "Error if method is not set"() {
when:
reader.apply('{"id":2, "jsonrpc":"2.0", "params":[]}'.bytes)
then:
def t = thrown(RpcException)
t.code == -32600
t.rpcMessage.toLowerCase() == "method is not set"
t.details == new JsonRpcResponse.IntId(2)
}
def "Error if params is not array"() {
when:
reader.apply('{"id":2, "jsonrpc":"2.0", "method":"test", "params":123}'.bytes)
then:
def t = thrown(RpcException)
t.code == -32600
t.rpcMessage.toLowerCase() == "params must be an array"
t.details == new JsonRpcResponse.IntId(2)
}
} }

View File

@@ -8,6 +8,9 @@ cluster:
upstreams: upstreams:
- id: test-1 - id: test-1
chain: ethereum chain: ethereum
methods:
enabled:
- name: debug_traceTransaction
options: options:
disable-validation: true disable-validation: true
connection: connection:

View File

@@ -5,9 +5,11 @@ import com.fasterxml.jackson.databind.ObjectMapper
class BlocksHandler implements CallHandler { class BlocksHandler implements CallHandler {
ObjectMapper objectMapper ObjectMapper objectMapper
ResourceResponse resourceResponse
BlocksHandler(ObjectMapper objectMapper) { BlocksHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper this.objectMapper = objectMapper
this.resourceResponse = new ResourceResponse(objectMapper)
} }
@Override @Override
@@ -18,22 +20,13 @@ class BlocksHandler implements CallHandler {
if (method == "eth_getBlockByNumber") { if (method == "eth_getBlockByNumber") {
String blockId = params[0] String blockId = params[0]
println("get block $blockId") println("get block $blockId")
def result = getResource("block-${blockId}.json") return resourceResponse.respondWith("block-${blockId}.json")
return Result.ok(result)
} }
if (method == "eth_getTransactionByHash") { if (method == "eth_getTransactionByHash") {
String txId = params[0] String txId = params[0]
def result = getResource("tx-${txId}.json") return resourceResponse.respondWith("tx-${txId}.json")
return Result.ok(result)
} }
return null return null
} }
Object getResource(String name) {
String json = BlocksHandler.class.getResourceAsStream("/" + name)?.text
if (json == null) {
return null
}
return objectMapper.readValue(json, Map)
}
} }

View File

@@ -0,0 +1,25 @@
package testing
import com.fasterxml.jackson.databind.ObjectMapper
class ResourceResponse {
ObjectMapper objectMapper
ResourceResponse(ObjectMapper objectMapper) {
this.objectMapper = objectMapper
}
Object getResource(String name) {
String json = BlocksHandler.class.getResourceAsStream("/" + name)?.text
if (json == null) {
return null
}
return objectMapper.readValue(json, Map)
}
CallHandler.Result respondWith(String name) {
return CallHandler.Result.ok(getResource(name))
}
}

View File

@@ -13,7 +13,7 @@ class SimpleUpstream {
void prepare() { void prepare() {
objectMapper = new ObjectMapper() objectMapper = new ObjectMapper()
handlers << new TestcaseHandler() handlers << new TestcaseHandler(objectMapper)
handlers << new CommonHandlers() handlers << new CommonHandlers()
handlers << new BlocksHandler(objectMapper) handlers << new BlocksHandler(objectMapper)
handlers << new InvalidCallHandler() handlers << new InvalidCallHandler()

View File

@@ -1,7 +1,17 @@
package testing package testing
import com.fasterxml.jackson.databind.ObjectMapper
class TestcaseHandler implements CallHandler { class TestcaseHandler implements CallHandler {
ObjectMapper objectMapper
ResourceResponse resourceResponse
TestcaseHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper
this.resourceResponse = new ResourceResponse(objectMapper)
}
@Override @Override
Result handle(String method, List<Object> params) { Result handle(String method, List<Object> params) {
// https://github.com/emeraldpay/dshackle/issues/35 // https://github.com/emeraldpay/dshackle/issues/35
@@ -9,6 +19,11 @@ class TestcaseHandler implements CallHandler {
&& params[0].to?.toLowerCase() == "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c".toLowerCase()) { && params[0].to?.toLowerCase() == "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c".toLowerCase()) {
return Result.error(-32015, "VM execution error.") return Result.error(-32015, "VM execution error.")
} }
// https://github.com/emeraldpay/dshackle/issues/43
if (method == "debug_traceTransaction"
&& params[0].toLowerCase() == "0xd949bc0fe1a5d16f4522bc47933554dcc4ada0493ff71ee1973b2410257af9fe".toLowerCase()) {
return resourceResponse.respondWith("trace-0xd949bc.json")
}
return null return null
} }
} }

File diff suppressed because it is too large Load Diff