Merge pull request #93 from emeraldpay/feat/accesslog

access logging
This commit is contained in:
Igor Artamonov
2021-07-19 23:24:04 -04:00
committed by GitHub
27 changed files with 1328 additions and 38 deletions

View File

@@ -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"
}
}
}

View File

@@ -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_"
}
}
}

View File

@@ -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"
}
}