solution: save remote client details into access log
This commit is contained in:
@@ -16,7 +16,6 @@
|
||||
package io.emeraldpay.dshackle.monitoring.accesslog
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.grpc.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -39,6 +38,7 @@ class AccessHandler(
|
||||
when (val method = call.methodDescriptor.bareMethodName) {
|
||||
"NativeCall" -> {
|
||||
val builder = Events.NativeCallBuilder()
|
||||
.start(headers, call.attributes)
|
||||
return OnNativeCall<ReqT, RespT>(
|
||||
next.startCall(OnNativeCallResponse(call, builder), headers),
|
||||
builder) { logs ->
|
||||
|
||||
@@ -56,7 +56,7 @@ class AccessLogWriter(
|
||||
@PostConstruct
|
||||
fun start() {
|
||||
if (!config.enabled) {
|
||||
log.info("Access Log is diabled")
|
||||
log.info("Access Log is disabled")
|
||||
return
|
||||
}
|
||||
log.info("Writing Access Log to ${filename.absolutePath}")
|
||||
|
||||
@@ -18,10 +18,15 @@ package io.emeraldpay.dshackle.monitoring.accesslog
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
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.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class Events {
|
||||
|
||||
@@ -65,11 +70,13 @@ class Events {
|
||||
|
||||
data class StreamRequestDetails(
|
||||
val id: UUID,
|
||||
val start: Instant
|
||||
val start: Instant,
|
||||
val remote: Remote
|
||||
)
|
||||
|
||||
data class Remote(
|
||||
val ips: List<String>,
|
||||
val ip: String,
|
||||
val userAgent: String
|
||||
)
|
||||
|
||||
@@ -88,15 +95,82 @@ class Events {
|
||||
|
||||
class NativeCallBuilder() {
|
||||
|
||||
private val requestDetails = StreamRequestDetails(
|
||||
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]+")
|
||||
}
|
||||
|
||||
private var requestDetails = StreamRequestDetails(
|
||||
UUID.randomUUID(),
|
||||
Instant.now()
|
||||
Instant.now(),
|
||||
Remote(emptyList(), "", "")
|
||||
)
|
||||
|
||||
var chain: Int = Chain.UNSPECIFIED.id
|
||||
val items = ArrayList<NativeCallItemDetails>()
|
||||
val replies = HashMap<Int, NativeCallReplyDetails>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fun start(metadata: Metadata, attributes: Attributes): NativeCallBuilder {
|
||||
val userAgent = metadata.get(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER))
|
||||
?.let(this@NativeCallBuilder::clean)
|
||||
?: ""
|
||||
val ips = ArrayList<InetAddress>()
|
||||
remoteIpKeys.forEach { key ->
|
||||
metadata.get(key)?.let {
|
||||
it.trim().ifEmpty { null }
|
||||
?.let(this@NativeCallBuilder::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 = Remote(
|
||||
ips = ips.map { it.hostAddress },
|
||||
ip = ip,
|
||||
userAgent = userAgent
|
||||
))
|
||||
return this
|
||||
}
|
||||
|
||||
fun withChain(chain: Int): NativeCallBuilder {
|
||||
this.chain = chain
|
||||
return this
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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 spock.lang.Specification
|
||||
|
||||
class EventsNativeCallBuilderSpec extends Specification {
|
||||
|
||||
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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0]) {
|
||||
it.request != null
|
||||
it.request.remote != null
|
||||
with(it.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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0].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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0].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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0].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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0].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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0].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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0].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 Events.NativeCallBuilder()
|
||||
.start(metadata, attributes)
|
||||
.withChain(Chain.ETHEREUM.id)
|
||||
.onItem(BlockchainOuterClass.NativeCallItem.getDefaultInstance())
|
||||
.build()
|
||||
then:
|
||||
act.size() == 1
|
||||
with(act[0].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_"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user