@@ -19,6 +19,8 @@ import com.fasterxml.jackson.core.Version
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.data.EsploraUnspent
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.data.EsploraUnspentDeserializer
|
||||
import io.emeraldpay.dshackle.upstream.bitcoin.data.RpcUnspent
|
||||
@@ -49,6 +51,8 @@ class Global {
|
||||
|
||||
val objectMapper = ObjectMapper()
|
||||
objectMapper.registerModule(module)
|
||||
objectMapper.registerModule(Jdk8Module())
|
||||
objectMapper.registerModule(JavaTimeModule())
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
objectMapper
|
||||
.setDateFormat(SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSS"))
|
||||
|
||||
@@ -17,16 +17,11 @@
|
||||
package io.emeraldpay.dshackle
|
||||
|
||||
import io.emeraldpay.dshackle.config.MainConfig
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandler
|
||||
import io.grpc.*
|
||||
import io.grpc.netty.GrpcSslContexts
|
||||
import io.grpc.netty.NettyServerBuilder
|
||||
import io.netty.handler.ssl.ClientAuth
|
||||
import io.netty.handler.ssl.SslContext
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.core.io.ResourceLoader
|
||||
import org.springframework.stereotype.Service
|
||||
import java.net.InetSocketAddress
|
||||
import javax.annotation.PostConstruct
|
||||
@@ -36,7 +31,8 @@ import javax.annotation.PreDestroy
|
||||
open class GrpcServer(
|
||||
@Autowired val rpcs: List<io.grpc.BindableService>,
|
||||
@Autowired val mainConfig: MainConfig,
|
||||
@Autowired val tlsSetup: TlsSetup
|
||||
@Autowired val tlsSetup: TlsSetup,
|
||||
@Autowired val accessHandler: AccessHandler
|
||||
) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(GrpcServer::class.java)
|
||||
@@ -50,6 +46,14 @@ open class GrpcServer(
|
||||
log.info("Listening Native gRPC on ${mainConfig.host}:${mainConfig.port}")
|
||||
val serverBuilder = NettyServerBuilder
|
||||
.forAddress(InetSocketAddress(mainConfig.host, mainConfig.port))
|
||||
.let {
|
||||
if (mainConfig.accessLogConfig.enabled) {
|
||||
it.intercept(accessHandler)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
tlsSetup.setupServer("Native gRPC", mainConfig.tls, true)?.let {
|
||||
serverBuilder.sslContext(it)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
class AccessLogConfig(
|
||||
val enabled: Boolean = false
|
||||
) {
|
||||
|
||||
var filename: String = "./access_log.jsonl"
|
||||
|
||||
companion object {
|
||||
|
||||
fun default(): AccessLogConfig {
|
||||
return disabled()
|
||||
}
|
||||
|
||||
fun disabled(): AccessLogConfig {
|
||||
return AccessLogConfig(
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
|
||||
class AccessLogReader : YamlConfigReader(), ConfigReader<AccessLogConfig> {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AccessLogReader::class.java)
|
||||
}
|
||||
|
||||
override fun read(input: MappingNode?): AccessLogConfig {
|
||||
return getMapping(input, "accessLog")?.let { node ->
|
||||
val enabled = getValueAsBool(node, "enabled") ?: false
|
||||
if (!enabled) {
|
||||
AccessLogConfig.disabled()
|
||||
} else {
|
||||
val config = AccessLogConfig(true)
|
||||
getValueAsString(node, "filename")?.let {
|
||||
config.filename = it
|
||||
}
|
||||
config
|
||||
}
|
||||
} ?: AccessLogConfig.default()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,5 +24,5 @@ class MainConfig {
|
||||
var upstreams: UpstreamsConfig? = null
|
||||
var tokens: TokensConfig? = null
|
||||
var monitoring: MonitoringConfig = MonitoringConfig.default()
|
||||
|
||||
var accessLogConfig: AccessLogConfig = AccessLogConfig.default()
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class MainConfigReader(
|
||||
private val cacheConfigReader = CacheConfigReader()
|
||||
private val tokensConfigReader = TokensConfigReader()
|
||||
private val monitoringConfigReader = MonitoringConfigReader()
|
||||
private val accessLogReader = AccessLogReader()
|
||||
|
||||
fun read(input: InputStream): MainConfig? {
|
||||
val configNode = readNode(input)
|
||||
@@ -67,6 +68,9 @@ class MainConfigReader(
|
||||
monitoringConfigReader.read(input).let {
|
||||
config.monitoring = it
|
||||
}
|
||||
accessLogReader.read(input).let {
|
||||
config.accessLogConfig = it
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* 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 io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.grpc.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class AccessHandler(
|
||||
@Autowired private val accessLogWriter: AccessLogWriter
|
||||
) : ServerInterceptor {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AccessHandler::class.java)
|
||||
}
|
||||
|
||||
override fun <ReqT : Any, RespT : Any> interceptCall(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>): ServerCall.Listener<ReqT> {
|
||||
|
||||
return when (val method = call.methodDescriptor.bareMethodName) {
|
||||
"SubscribeHead" -> processSubscribeHead(call, headers, next)
|
||||
"SubscribeBalance" -> processSubscribeBalance(call, headers, next, true)
|
||||
"SubscribeTxStatus" -> processSubscribeTxStatus(call, headers, next)
|
||||
"GetBalance" -> processSubscribeBalance(call, headers, next, false)
|
||||
"NativeCall" -> processNativeCall(call, headers, next)
|
||||
"Describe" -> processDescribe(call, headers, next)
|
||||
"SubscribeStatus" -> processStatus(call, headers, next)
|
||||
else -> {
|
||||
log.warn("unsupported method `{}`", method)
|
||||
next.startCall(call, headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <ReqT : Any, RespT : Any, E> process(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>,
|
||||
builder: EventsBuilder.RequestReply<E, ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
builder.start(headers, call.attributes)
|
||||
val callWrapper: ServerCall<ReqT, RespT> = StdCallResponse(
|
||||
call, builder, accessLogWriter
|
||||
)
|
||||
return StdCallListener(
|
||||
next.startCall(callWrapper, headers),
|
||||
builder
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <ReqT : Any, RespT : Any> processSubscribeHead(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return process(call, headers, next,
|
||||
EventsBuilder.SubscribeHead() as EventsBuilder.RequestReply<*, ReqT, RespT>
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <ReqT : Any, RespT : Any> processSubscribeBalance(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>,
|
||||
subscribe: Boolean
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return process(call, headers, next,
|
||||
EventsBuilder.SubscribeBalance(subscribe) as EventsBuilder.RequestReply<*, ReqT, RespT>
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <ReqT : Any, RespT : Any> processSubscribeTxStatus(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return process(call, headers, next,
|
||||
EventsBuilder.TxStatus() as EventsBuilder.RequestReply<*, ReqT, RespT>
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <ReqT : Any, RespT : Any> processNativeCall(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return process(call, headers, next,
|
||||
EventsBuilder.NativeCall() as EventsBuilder.RequestReply<*, ReqT, RespT>
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <ReqT : Any, RespT : Any> processDescribe(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return process(call, headers, next,
|
||||
EventsBuilder.Describe() as EventsBuilder.RequestReply<*, ReqT, RespT>
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <ReqT : Any, RespT : Any> processStatus(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata,
|
||||
next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return process(call, headers, next,
|
||||
EventsBuilder.Status() as EventsBuilder.RequestReply<*, ReqT, RespT>
|
||||
)
|
||||
}
|
||||
|
||||
open class StdCallListener<Req, EB : EventsBuilder.RequestReply<*, Req, *>>(
|
||||
val next: ServerCall.Listener<Req>,
|
||||
val builder: EB
|
||||
) : ForwardingServerCallListener<Req>() {
|
||||
|
||||
override fun onMessage(message: Req) {
|
||||
builder.onRequest(message)
|
||||
super.onMessage(message)
|
||||
}
|
||||
|
||||
override fun delegate(): ServerCall.Listener<Req> {
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
open class StdCallResponse<ReqT : Any, RespT : Any, EB : EventsBuilder.RequestReply<*, ReqT, RespT>>(
|
||||
val next: ServerCall<ReqT, RespT>,
|
||||
val builder: EB,
|
||||
val accessLogWriter: AccessLogWriter
|
||||
) : ForwardingServerCall<ReqT, RespT>() {
|
||||
|
||||
override fun getMethodDescriptor(): MethodDescriptor<ReqT, RespT> {
|
||||
return next.methodDescriptor
|
||||
}
|
||||
|
||||
override fun delegate(): ServerCall<ReqT, RespT> {
|
||||
return next
|
||||
}
|
||||
|
||||
override fun sendMessage(message: RespT) {
|
||||
super.sendMessage(message)
|
||||
accessLogWriter.submit(
|
||||
builder.onReply(message)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* 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 io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.config.MainConfig
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.io.*
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.annotation.PostConstruct
|
||||
|
||||
@Repository
|
||||
class AccessLogWriter(
|
||||
@Autowired mainConfig: MainConfig
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AccessLogWriter::class.java)
|
||||
private const val WRITE_BATCH_LIMIT = 5000
|
||||
private const val FLUSH_SLEEP_MS = 500L
|
||||
private const val START_SLEEP_MS = 2000L
|
||||
private val NL = "\n".toByteArray()
|
||||
}
|
||||
|
||||
private val config = mainConfig.accessLogConfig
|
||||
private val filename = File(config.filename)
|
||||
private val scheduler = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
private val queue = ConcurrentLinkedQueue<Any>()
|
||||
private val objectMapper = Global.objectMapper
|
||||
private var lastErrorAt: Instant = Instant.ofEpochMilli(0)
|
||||
|
||||
private val runner = Runnable {
|
||||
flushRunner()
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun start() {
|
||||
if (!config.enabled) {
|
||||
log.info("Access Log is disabled")
|
||||
return
|
||||
}
|
||||
log.info("Writing Access Log to ${filename.absolutePath}")
|
||||
scheduler.schedule(runner, START_SLEEP_MS, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
private fun flushRunner() {
|
||||
try {
|
||||
flush()
|
||||
} catch (t: Throwable) {
|
||||
logError {
|
||||
log.error("Failed to write logs. ${t.javaClass}:${t.message}")
|
||||
}
|
||||
} finally {
|
||||
scheduler.schedule(runner, FLUSH_SLEEP_MS, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
fun submit(event: Any) {
|
||||
queue.add(event)
|
||||
}
|
||||
|
||||
fun submit(events: List<Any>) {
|
||||
queue.addAll(events)
|
||||
}
|
||||
|
||||
fun logError(m: () -> Unit) {
|
||||
val now = Instant.now()
|
||||
if (lastErrorAt.isBefore(now - Duration.ofMinutes(1))) {
|
||||
lastErrorAt = now
|
||||
m()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun flush() {
|
||||
if (!filename.exists()) {
|
||||
if (!filename.createNewFile()) {
|
||||
logError {
|
||||
log.error("Cannot create Access Log file at ${filename.absolutePath}")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
BufferedOutputStream(FileOutputStream(filename, true)).use { wrt ->
|
||||
var limit = WRITE_BATCH_LIMIT
|
||||
while (limit > 0) {
|
||||
limit--
|
||||
val next = queue.poll() ?: return
|
||||
val bytes: ByteArray? = try {
|
||||
objectMapper.writeValueAsBytes(next)
|
||||
} catch (t: Throwable) {
|
||||
logError {
|
||||
log.warn("Failed to write an access log line. ${t.message}")
|
||||
}
|
||||
null
|
||||
}
|
||||
if (bytes != null && bytes.isNotEmpty()) {
|
||||
wrt.write(bytes)
|
||||
wrt.write(NL, 0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* 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 io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class Events {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(Events::class.java)
|
||||
}
|
||||
|
||||
abstract class Base(
|
||||
val id: UUID,
|
||||
val method: String
|
||||
) {
|
||||
val version = "accesslog/v1beta"
|
||||
val ts = Instant.now()
|
||||
}
|
||||
|
||||
abstract class ChainBase(
|
||||
val blockchain: Chain, method: String, id: UUID
|
||||
) : Base(id, method)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class SubscribeHead(
|
||||
blockchain: Chain, id: UUID,
|
||||
// initial request details
|
||||
val request: StreamRequestDetails,
|
||||
// index of the current response
|
||||
val index: Int
|
||||
) : ChainBase(blockchain, "SubscribeHead", id)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class SubscribeBalance(
|
||||
blockchain: Chain, id: UUID, subscribe: Boolean,
|
||||
// initial request details
|
||||
val request: StreamRequestDetails,
|
||||
val balanceRequest: BalanceRequest,
|
||||
val addressBalance: AddressBalance,
|
||||
// index of the current response
|
||||
val index: Int
|
||||
) : ChainBase(blockchain, if (subscribe) "SubscribeBalance" else "GetBalance", id)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class TxStatus(
|
||||
blockchain: Chain, id: UUID,
|
||||
val request: StreamRequestDetails,
|
||||
val txStatusRequest: TxStatusRequest,
|
||||
val txStatus: TxStatusResponse,
|
||||
// index of the current response
|
||||
val index: Int
|
||||
) : ChainBase(blockchain, "SubscribeTxStatus", id)
|
||||
|
||||
data class TxStatusRequest(
|
||||
val txId: String
|
||||
)
|
||||
|
||||
data class TxStatusResponse(
|
||||
val confirmations: Int
|
||||
)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class NativeCall(
|
||||
blockchain: Chain, id: UUID,
|
||||
|
||||
// info about the initial request, that may include several native calls
|
||||
val request: StreamRequestDetails,
|
||||
// total native calls passes within the initial request
|
||||
val total: Int,
|
||||
// index of the call specific for the current response
|
||||
val index: Int,
|
||||
val selector: String? = null,
|
||||
val quorum: Long? = null,
|
||||
val minAvailability: String? = null,
|
||||
|
||||
val succeed: Boolean,
|
||||
val rpcError: Int? = null,
|
||||
val payloadSizeBytes: Long,
|
||||
val nativeCall: NativeCallItemDetails
|
||||
) : ChainBase(blockchain, "NativeCall", id)
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class Describe(
|
||||
id: UUID,
|
||||
val request: StreamRequestDetails
|
||||
) : Base(id, "Describe")
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class Status(
|
||||
blockchain: Chain, id: UUID,
|
||||
val request: StreamRequestDetails
|
||||
) : ChainBase(blockchain, "Status", id)
|
||||
|
||||
data class StreamRequestDetails(
|
||||
val id: UUID,
|
||||
val start: Instant,
|
||||
val remote: Remote
|
||||
)
|
||||
|
||||
data class Remote(
|
||||
val ips: List<String>,
|
||||
val ip: String,
|
||||
val userAgent: String
|
||||
)
|
||||
|
||||
data class NativeCallItemDetails(
|
||||
val method: String,
|
||||
val id: Int,
|
||||
val payloadSizeBytes: Long
|
||||
)
|
||||
|
||||
data class NativeCallReplyDetails(
|
||||
val id: Int,
|
||||
val succeed: Boolean,
|
||||
val replySizeBytes: Long,
|
||||
val ts: Instant = Instant.now()
|
||||
)
|
||||
|
||||
data class BalanceRequest(
|
||||
val asset: String,
|
||||
val addressType: String
|
||||
)
|
||||
|
||||
data class AddressBalance(
|
||||
val asset: String,
|
||||
val address: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* 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 io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.grpc.Attributes
|
||||
import io.grpc.Grpc
|
||||
import io.grpc.Metadata
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class EventsBuilder {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(EventsBuilder::class.java)
|
||||
}
|
||||
|
||||
interface StartingRequest {
|
||||
fun start(metadata: Metadata, attributes: Attributes)
|
||||
}
|
||||
|
||||
interface RequestReply<E, Req, Resp> : StartingRequest {
|
||||
fun onRequest(msg: Req)
|
||||
fun onReply(msg: Resp): E
|
||||
}
|
||||
|
||||
abstract class Base<T>() : StartingRequest {
|
||||
companion object {
|
||||
private val remoteIpKeys = listOf(
|
||||
Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER),
|
||||
Metadata.Key.of("x-forwarded-for", Metadata.ASCII_STRING_MARSHALLER)
|
||||
)
|
||||
private val invalidCharacters = Regex("[\n\t]+")
|
||||
}
|
||||
|
||||
var requestDetails = Events.StreamRequestDetails(
|
||||
UUID.randomUUID(),
|
||||
Instant.now(),
|
||||
Events.Remote(emptyList(), "", "")
|
||||
)
|
||||
|
||||
var chainId: Int = Chain.UNSPECIFIED.id
|
||||
var chain = Chain.UNSPECIFIED
|
||||
|
||||
private fun toInetAddress(ip: String): InetAddress? {
|
||||
val isIp = Character.digit(ip[0], 16) != -1
|
||||
if (!isIp) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
InetAddress.getByName(ip)
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun findBestIp(ips: List<InetAddress>): InetAddress? {
|
||||
// check if a real remote address is provided, otherwise use any local address
|
||||
return ips.sortedWith(kotlin.Comparator { a, b ->
|
||||
val aLocal = a.isLoopbackAddress || a.isSiteLocalAddress
|
||||
val bLocal = b.isLoopbackAddress || b.isSiteLocalAddress
|
||||
when {
|
||||
aLocal && bLocal -> 0
|
||||
aLocal -> 1
|
||||
else -> -1
|
||||
}
|
||||
}).firstOrNull()
|
||||
}
|
||||
|
||||
private fun clean(s: String): String {
|
||||
return StringUtils.truncate(s, 128)
|
||||
.replace(invalidCharacters, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
protected abstract fun getT(): T
|
||||
|
||||
override fun start(metadata: Metadata, attributes: Attributes) {
|
||||
val userAgent = metadata.get(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER))
|
||||
?.let(this@Base::clean)
|
||||
?: ""
|
||||
val ips = ArrayList<InetAddress>()
|
||||
remoteIpKeys.forEach { key ->
|
||||
metadata.get(key)?.let {
|
||||
it.trim().ifEmpty { null }
|
||||
?.let(this@Base::toInetAddress)
|
||||
?.let(ips::add)
|
||||
}
|
||||
}
|
||||
attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)?.let { addr ->
|
||||
if (addr is InetSocketAddress) {
|
||||
ips.add(addr.address)
|
||||
}
|
||||
}
|
||||
val ip = findBestIp(ips)?.hostAddress ?: ""
|
||||
this.requestDetails = this.requestDetails
|
||||
.copy(remote = Events.Remote(
|
||||
ips = ips.map { it.hostAddress },
|
||||
ip = ip,
|
||||
userAgent = userAgent
|
||||
))
|
||||
}
|
||||
|
||||
fun withChain(chain: Int): T {
|
||||
this.chainId = chain
|
||||
this.chain = Chain.byId(chainId)
|
||||
return getT()
|
||||
}
|
||||
}
|
||||
|
||||
class SubscribeHead() :
|
||||
Base<SubscribeHead>(),
|
||||
RequestReply<Events.SubscribeHead, Common.Chain, BlockchainOuterClass.ChainHead> {
|
||||
|
||||
private var index = 0
|
||||
|
||||
override fun getT(): SubscribeHead {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onRequest(msg: Common.Chain) {
|
||||
withChain(msg.type.number)
|
||||
}
|
||||
|
||||
override fun onReply(msg: BlockchainOuterClass.ChainHead): Events.SubscribeHead {
|
||||
return Events.SubscribeHead(
|
||||
chain, UUID.randomUUID(), requestDetails, index++
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SubscribeBalance(val subscribe: Boolean) :
|
||||
Base<SubscribeBalance>(),
|
||||
RequestReply<Events.SubscribeBalance, BlockchainOuterClass.BalanceRequest, BlockchainOuterClass.AddressBalance> {
|
||||
|
||||
private var index = 0
|
||||
private var balanceRequest: Events.BalanceRequest? = null
|
||||
|
||||
override fun getT(): SubscribeBalance {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onRequest(msg: BlockchainOuterClass.BalanceRequest) {
|
||||
balanceRequest = Events.BalanceRequest(
|
||||
msg.asset.code.toUpperCase(),
|
||||
msg.address.addrTypeCase.name
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReply(msg: BlockchainOuterClass.AddressBalance): Events.SubscribeBalance {
|
||||
if (balanceRequest == null) {
|
||||
throw IllegalStateException("Request is not initialized")
|
||||
}
|
||||
val addressBalance = Events.AddressBalance(msg.asset.code, msg.address.address)
|
||||
val chain = Chain.byId(msg.asset.chain.number)
|
||||
return Events.SubscribeBalance(
|
||||
chain, UUID.randomUUID(), subscribe, requestDetails, balanceRequest!!, addressBalance, index++
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class TxStatus() :
|
||||
Base<TxStatus>(),
|
||||
RequestReply<Events.TxStatus, BlockchainOuterClass.TxStatusRequest, BlockchainOuterClass.TxStatus> {
|
||||
private var index = 0
|
||||
private var txStatusRequest: Events.TxStatusRequest? = null
|
||||
|
||||
override fun onRequest(msg: BlockchainOuterClass.TxStatusRequest) {
|
||||
this.txStatusRequest = Events.TxStatusRequest(msg.txId)
|
||||
withChain(msg.chainValue)
|
||||
}
|
||||
|
||||
override fun onReply(msg: BlockchainOuterClass.TxStatus): Events.TxStatus {
|
||||
return Events.TxStatus(
|
||||
chain, UUID.randomUUID(), requestDetails, txStatusRequest!!,
|
||||
Events.TxStatusResponse(msg.confirmations),
|
||||
index++
|
||||
)
|
||||
}
|
||||
|
||||
override fun getT(): TxStatus {
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NativeCall :
|
||||
Base<NativeCall>(),
|
||||
RequestReply<Events.NativeCall, BlockchainOuterClass.NativeCallRequest, BlockchainOuterClass.NativeCallReplyItem> {
|
||||
val items = ArrayList<Events.NativeCallItemDetails>()
|
||||
val replies = HashMap<Int, Events.NativeCallReplyDetails>()
|
||||
private var index = 0
|
||||
|
||||
override fun getT(): NativeCall {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onRequest(msg: BlockchainOuterClass.NativeCallRequest) {
|
||||
withChain(msg.chain.number)
|
||||
msg.itemsList.forEach { item ->
|
||||
this.items.add(
|
||||
Events.NativeCallItemDetails(
|
||||
item.method,
|
||||
item.id,
|
||||
item.payload.size().toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReply(msg: BlockchainOuterClass.NativeCallReplyItem): Events.NativeCall {
|
||||
val item = items.find { it.id == msg.id }!!
|
||||
return Events.NativeCall(
|
||||
request = requestDetails,
|
||||
total = items.size,
|
||||
index = index++,
|
||||
succeed = msg.succeed,
|
||||
blockchain = chain,
|
||||
nativeCall = item,
|
||||
payloadSizeBytes = item.payloadSizeBytes,
|
||||
id = UUID.randomUUID()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Describe :
|
||||
Base<Describe>(),
|
||||
RequestReply<Events.Describe, BlockchainOuterClass.DescribeRequest, BlockchainOuterClass.DescribeResponse> {
|
||||
|
||||
override fun getT(): Describe {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onRequest(msg: BlockchainOuterClass.DescribeRequest) {
|
||||
}
|
||||
|
||||
override fun onReply(msg: BlockchainOuterClass.DescribeResponse): Events.Describe {
|
||||
return Events.Describe(
|
||||
id = UUID.randomUUID(),
|
||||
request = requestDetails
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Status :
|
||||
Base<Status>(),
|
||||
RequestReply<Events.Status, BlockchainOuterClass.StatusRequest, BlockchainOuterClass.ChainStatus> {
|
||||
override fun getT(): Status {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onRequest(msg: BlockchainOuterClass.StatusRequest) {
|
||||
}
|
||||
|
||||
override fun onReply(msg: BlockchainOuterClass.ChainStatus): Events.Status {
|
||||
val chain = Chain.byId(msg.chainValue)
|
||||
return Events.Status(
|
||||
blockchain = chain,
|
||||
request = requestDetails,
|
||||
id = UUID.randomUUID()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.startup.QuorumForLabels
|
||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
import reactor.extra.processor.TopicProcessor
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
@@ -41,7 +42,9 @@ abstract class DefaultUpstream(
|
||||
this(id, Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE, options, role, targets, node)
|
||||
|
||||
private val status = AtomicReference(Status(defaultLag, defaultAvail, statusByLag(defaultLag, defaultAvail)))
|
||||
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
||||
private val statusStream = Sinks.many()
|
||||
.multicast()
|
||||
.directBestEffort<UpstreamAvailability>()
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return getStatus() == UpstreamAvailability.OK
|
||||
@@ -63,6 +66,7 @@ abstract class DefaultUpstream(
|
||||
status.updateAndGet { curr ->
|
||||
Status(curr.lag, avail, statusByLag(curr.lag, avail))
|
||||
}
|
||||
statusStream.tryEmitNext(status.get().status)
|
||||
}
|
||||
|
||||
fun statusByLag(lag: Long, proposed: UpstreamAvailability): UpstreamAvailability {
|
||||
@@ -76,7 +80,8 @@ abstract class DefaultUpstream(
|
||||
}
|
||||
|
||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||
return Flux.from(statusStream)
|
||||
return statusStream.asFlux()
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
override fun setLag(lag: Long) {
|
||||
@@ -86,6 +91,7 @@ abstract class DefaultUpstream(
|
||||
status.updateAndGet { curr ->
|
||||
Status(lag, curr.avail, statusByLag(lag, curr.avail))
|
||||
}
|
||||
statusStream.tryEmitNext(status.get().status)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,12 @@ abstract class Multistream(
|
||||
}
|
||||
|
||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
|
||||
val upstreamsFluxes = getAll().map { up ->
|
||||
Flux.concat(
|
||||
Mono.just(up.getStatus()),
|
||||
up.observeStatus()
|
||||
).map { UpstreamStatus(up, it) }
|
||||
}
|
||||
return Flux.merge(upstreamsFluxes)
|
||||
.filter(FilterBestAvailability())
|
||||
.map { it.status }
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.config.AccessLogConfig
|
||||
import io.emeraldpay.dshackle.config.MainConfig
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
class AccessLogWriterSpec extends Specification {
|
||||
|
||||
def "writes log event"() {
|
||||
setup:
|
||||
File dir = File.createTempDir("dshackle-test-")
|
||||
File accessLog = new File(dir, "accesslog.jsonl")
|
||||
println("Write access log to $accessLog.absolutePath")
|
||||
MainConfig config = new MainConfig()
|
||||
config.accessLogConfig = new AccessLogConfig(true).tap {
|
||||
it.filename = accessLog.absolutePath
|
||||
}
|
||||
AccessLogWriter logWriter = new AccessLogWriter(config)
|
||||
|
||||
when:
|
||||
def event = new Events.Status(
|
||||
Chain.ETHEREUM, UUID.fromString("9d8ecbf3-12fb-49cf-af9d-949a1050a000"),
|
||||
new Events.StreamRequestDetails(
|
||||
UUID.fromString("9d8ecbf3-12fb-49cf-af9d-949a1050a000"),
|
||||
Instant.ofEpochMilli(1626746880123),
|
||||
new Events.Remote(
|
||||
["127.0.0.1", "172.217.8.78"], "172.217.8.78", "UnitTest"
|
||||
)
|
||||
)
|
||||
)
|
||||
logWriter.submit([event])
|
||||
logWriter.flush()
|
||||
def act = accessLog.readLines()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0]) {
|
||||
def json = Global.objectMapper.readValue(it, Map)
|
||||
json["version"] == "accesslog/v1beta"
|
||||
json["id"] == "9d8ecbf3-12fb-49cf-af9d-949a1050a000"
|
||||
json["method"] == "Status"
|
||||
json["blockchain"] == "ETHEREUM"
|
||||
json["request"]["start"] == "2021-07-20T02:08:00.123Z"
|
||||
json["request"]["id"] == "9d8ecbf3-12fb-49cf-af9d-949a1050a000"
|
||||
json["request"]["remote"]["ip"] == "172.217.8.78"
|
||||
json["request"]["remote"]["userAgent"] == "UnitTest"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* 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 io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import io.grpc.Attributes
|
||||
import io.grpc.Grpc
|
||||
import io.grpc.Metadata
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import org.junit.validator.TestClassValidator
|
||||
import spock.lang.Specification
|
||||
|
||||
class EventsBaseBuilderSpec extends Specification {
|
||||
|
||||
class TestEvent extends Events.Base {
|
||||
Events.StreamRequestDetails request
|
||||
|
||||
TestEvent(Events.StreamRequestDetails request) {
|
||||
super(UUID.randomUUID(), "TEST")
|
||||
this.request = request
|
||||
}
|
||||
}
|
||||
|
||||
class TestEventBuilder extends EventsBuilder.Base<TestEventBuilder>
|
||||
implements EventsBuilder.RequestReply<TestEvent, BlockchainOuterClass.NativeCallRequest, BlockchainOuterClass.NativeCallReplyItem> {
|
||||
|
||||
@Override
|
||||
protected TestEventBuilder getT() {
|
||||
return this
|
||||
}
|
||||
|
||||
@Override
|
||||
void onRequest(BlockchainOuterClass.NativeCallRequest msg) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
TestEvent onReply(BlockchainOuterClass.NativeCallReplyItem msg) {
|
||||
return new TestEvent(requestDetails)
|
||||
}
|
||||
}
|
||||
|
||||
def "Parse headers from direct local access"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("127.0.0.1"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
act.request != null
|
||||
act.request.remote != null
|
||||
with(act.request.remote) {
|
||||
ips == ["127.0.0.1"]
|
||||
userAgent == "grpc-go/1.30.0"
|
||||
ip == "127.0.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
def "Extracts real remote ip"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
|
||||
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "30.56.100.15")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("127.0.0.1"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
with(act.request.remote) {
|
||||
ips == ["30.56.100.15", "127.0.0.1"]
|
||||
ip == "30.56.100.15"
|
||||
}
|
||||
}
|
||||
|
||||
def "Ignores remote ip header if already connected from remote"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
|
||||
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "192.168.1.1")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
with(act.request.remote) {
|
||||
ips == ["192.168.1.1", "30.56.100.15"]
|
||||
userAgent == "grpc-go/1.30.0"
|
||||
ip == "30.56.100.15"
|
||||
}
|
||||
}
|
||||
|
||||
def "Ignores invalid ip header"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
|
||||
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "271.194.19.1")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
with(act.request.remote) {
|
||||
ips == ["30.56.100.15"]
|
||||
userAgent == "grpc-go/1.30.0"
|
||||
ip == "30.56.100.15"
|
||||
}
|
||||
}
|
||||
|
||||
def "Ignores host addr in ip header"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
|
||||
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "google.com")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
with(act.request.remote) {
|
||||
ips == ["30.56.100.15"]
|
||||
userAgent == "grpc-go/1.30.0"
|
||||
ip == "30.56.100.15"
|
||||
}
|
||||
}
|
||||
|
||||
def "Extracts ipv6 addresses"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0")
|
||||
metadata.put(Metadata.Key.of("x-real-ip", Metadata.ASCII_STRING_MARSHALLER), "2001:0db8:0000:0000:0000:ff00:0042:8329")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet6Address.getByName("::1"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
with(act.request.remote) {
|
||||
ips == ["2001:db8:0:0:0:ff00:42:8329", "0:0:0:0:0:0:0:1"]
|
||||
ip == "2001:db8:0:0:0:ff00:42:8329"
|
||||
}
|
||||
}
|
||||
|
||||
def "Cleans up user agent"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "grpc-go/1.30.0\nxss\n\r")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
with(act.request.remote) {
|
||||
userAgent == "grpc-go/1.30.0 xss"
|
||||
}
|
||||
}
|
||||
|
||||
def "Truncates up user agent to 128 characters max"() {
|
||||
setup:
|
||||
def metadata = new Metadata()
|
||||
metadata.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER),
|
||||
"0123456_1_0123456_2_0123456_3_0123456_4_0123456_5_0123456_6_0123456_7_0123456_8_0123456_9_0123456_0_0123456_1_0123456_2_0123456_3_0123456_4_0123456_5")
|
||||
def attributes = Attributes.newBuilder()
|
||||
.set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, new InetSocketAddress(Inet4Address.getByName("30.56.100.15"), 2448))
|
||||
.build()
|
||||
when:
|
||||
def act = new TestEventBuilder()
|
||||
.tap {
|
||||
it.start(metadata, attributes)
|
||||
it.onRequest(BlockchainOuterClass.NativeCallRequest.getDefaultInstance())
|
||||
}
|
||||
.onReply(BlockchainOuterClass.NativeCallReplyItem.getDefaultInstance())
|
||||
then:
|
||||
with(act.request.remote) {
|
||||
userAgent.length() == 128
|
||||
userAgent == "0123456_1_0123456_2_0123456_3_0123456_4_0123456_5_0123456_6_0123456_7_0123456_8_0123456_9_0123456_0_0123456_1_0123456_2_0123456_"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import spock.lang.Specification
|
||||
|
||||
class EventsBuilderSubscribeBalanceSpec extends Specification {
|
||||
|
||||
def "Basic ethereum event"() {
|
||||
setup:
|
||||
def request = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||
.setAddress(
|
||||
Common.AnyAddress.newBuilder()
|
||||
.setAddressSingle(
|
||||
Common.SingleAddress.newBuilder()
|
||||
.setAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")
|
||||
)
|
||||
)
|
||||
.setAsset(
|
||||
Common.Asset.newBuilder()
|
||||
.setChainValue(100)
|
||||
.setCode("ETHER")
|
||||
)
|
||||
.build()
|
||||
def resp = BlockchainOuterClass.AddressBalance.newBuilder()
|
||||
.setAddress(Common.SingleAddress.newBuilder()
|
||||
.setAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"))
|
||||
.setAsset(Common.Asset.newBuilder()
|
||||
.setChainValue(100)
|
||||
.setCode("ETHER"))
|
||||
.setBalance("1234560000000000000")
|
||||
.build()
|
||||
when:
|
||||
def act = new EventsBuilder.SubscribeBalance(true).tap {
|
||||
it.onRequest(request)
|
||||
}.onReply(resp)
|
||||
then:
|
||||
act.index == 0
|
||||
act.blockchain == Chain.ETHEREUM
|
||||
act.balanceRequest.asset == "ETHER"
|
||||
act.balanceRequest.addressType == "ADDRESS_SINGLE"
|
||||
act.addressBalance.asset == "ETHER"
|
||||
act.addressBalance.address == "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
|
||||
}
|
||||
|
||||
def "Basic bitcoin event"() {
|
||||
setup:
|
||||
def request = BlockchainOuterClass.BalanceRequest.newBuilder()
|
||||
.setAddress(
|
||||
Common.AnyAddress.newBuilder()
|
||||
.setAddressSingle(
|
||||
Common.SingleAddress.newBuilder()
|
||||
.setAddress("1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s")
|
||||
)
|
||||
)
|
||||
.setAsset(
|
||||
Common.Asset.newBuilder()
|
||||
.setChainValue(1)
|
||||
.setCode("BTC")
|
||||
)
|
||||
.build()
|
||||
def resp = BlockchainOuterClass.AddressBalance.newBuilder()
|
||||
.setAddress(Common.SingleAddress.newBuilder()
|
||||
.setAddress("1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s"))
|
||||
.setAsset(Common.Asset.newBuilder()
|
||||
.setChainValue(1)
|
||||
.setCode("BTC"))
|
||||
.setBalance("12345600000000")
|
||||
.build()
|
||||
when:
|
||||
def act = new EventsBuilder.SubscribeBalance(true).tap {
|
||||
it.onRequest(request)
|
||||
}.onReply(resp)
|
||||
then:
|
||||
act.index == 0
|
||||
act.blockchain == Chain.BITCOIN
|
||||
act.balanceRequest.asset == "BTC"
|
||||
act.balanceRequest.addressType == "ADDRESS_SINGLE"
|
||||
act.addressBalance.asset == "BTC"
|
||||
act.addressBalance.address == "1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user