diff --git a/docs/reference-configuration.adoc b/docs/reference-configuration.adoc index 5deeaf1d..f6a35348 100644 --- a/docs/reference-configuration.adoc +++ b/docs/reference-configuration.adoc @@ -849,6 +849,18 @@ rpc: password: "${ETH_PASSWORD}" ---- +| `rpc.bearer-auth` + `rpc.bearer-auth.token` +a| HTTP Bearer token authorization (`Authorization: Bearer ` header), if required by the remote server. + +Cannot be used together with `basic-auth`. +Value can also reference env variables, for example: +[source,yaml] +---- +rpc: + url: "https://ethereum.com:8545" + bearer-auth: + token: "${ETH_TOKEN}" +---- + | `ws.url` | WebSocket URL to connect to. Optional, but optimizes performance if it's available. @@ -859,6 +871,9 @@ Optional, but optimizes performance if it's available. | `ws.basic-auth` + ... | WebSocket Basic Auth configuration, if required by the remote server +| `ws.bearer-auth` + `ws.bearer-auth.token` +| WebSocket Bearer token authorization, if required by the remote server + | `ws.frameSize` | WebSocket frame size limit. Ex `1kb`, `1024` (same as `1kb), `2mb`, etc. diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfig.kt index 8c30950d..21d0c69c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfig.kt @@ -33,6 +33,10 @@ class AuthConfig { val password: String, ) : ClientAuth() + class ClientBearerAuth( + val token: String, + ) : ClientAuth() + class ClientTlsAuth( var ca: String? = null, var certificate: String? = null, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfigReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfigReader.kt index 7545904c..027c5a3d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfigReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfigReader.kt @@ -40,6 +40,18 @@ class AuthConfigReader : YamlConfigReader() { } } + fun readClientBearerAuth(node: MappingNode?): AuthConfig.ClientBearerAuth? { + return getMapping(node, "bearer-auth")?.let { authNode -> + val token = getValueAsString(authNode, "token") + if (token != null) { + AuthConfig.ClientBearerAuth(token) + } else { + log.warn("Bearer auth is not fully configured, token is required") + null + } + } + } + fun readClientTls(node: MappingNode?): AuthConfig.ClientTlsAuth? { return getMapping(node, "tls")?.let { authNode -> val auth = AuthConfig.ClientTlsAuth() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt index 9aff5935..5c333c78 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfig.kt @@ -136,12 +136,14 @@ data class UpstreamsConfig( constructor(url: URI) : this(url, DEFAULT_MAX_CONNECTIONS, DEFAULT_QUEUE_SIZE) var basicAuth: AuthConfig.ClientBasicAuth? = null + var bearerAuth: AuthConfig.ClientBearerAuth? = null var tls: AuthConfig.ClientTlsAuth? = null } data class WsEndpoint(val url: URI) { var origin: URI? = null var basicAuth: AuthConfig.ClientBasicAuth? = null + var bearerAuth: AuthConfig.ClientBearerAuth? = null var frameSize: Int? = null var msgSize: Int? = null var connections: Int = 1 diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt index dfcad899..c51a493c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/UpstreamsConfigReader.kt @@ -147,6 +147,7 @@ class UpstreamsConfigReader( getValueAsString(node, "url")?.let { url -> val http = UpstreamsConfig.HttpEndpoint(URI(url), DEFAULT_MAX_CONNECTIONS, DEFAULT_QUEUE_SIZE) http.basicAuth = authConfigReader.readClientBasicAuth(node) + http.bearerAuth = readBearerAuth(node, http.basicAuth, url) http.tls = authConfigReader.readClientTls(node) connection.esplora = http } @@ -173,6 +174,19 @@ class UpstreamsConfigReader( return connection } + private fun readBearerAuth( + node: MappingNode?, + basicAuth: AuthConfig.ClientBasicAuth?, + url: String, + ): AuthConfig.ClientBearerAuth? { + val bearerAuth = authConfigReader.readClientBearerAuth(node) + if (bearerAuth != null && basicAuth != null) { + log.warn("Both basic-auth and bearer-auth are configured for $url, basic-auth is used") + return null + } + return bearerAuth + } + private fun readRpcConfig(connConfigNode: MappingNode): UpstreamsConfig.HttpEndpoint? { return getMapping(connConfigNode, "rpc")?.let { node -> val maxConnections = getValueAsInt(node, "max-connections") ?: DEFAULT_MAX_CONNECTIONS @@ -181,6 +195,7 @@ class UpstreamsConfigReader( getValueAsString(node, "url")?.let { url -> val http = UpstreamsConfig.HttpEndpoint(URI(url), maxConnections, queueSize) http.basicAuth = authConfigReader.readClientBasicAuth(node) + http.bearerAuth = readBearerAuth(node, http.basicAuth, url) http.tls = authConfigReader.readClientTls(node) http } @@ -224,6 +239,7 @@ class UpstreamsConfigReader( ws.origin = URI(origin) } ws.basicAuth = authConfigReader.readClientBasicAuth(node) + ws.bearerAuth = readBearerAuth(node, ws.basicAuth, url) getValueAsBytes(node, "frameSize")?.let { if (it < 65_535) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/BitcoinUpstreamCreator.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/BitcoinUpstreamCreator.kt index 2579466d..e16a2e8e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/BitcoinUpstreamCreator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/BitcoinUpstreamCreator.kt @@ -52,7 +52,7 @@ class BitcoinUpstreamCreator( fileResolver.resolve(ca).readBytes() } } - EsploraClient(endpoint.url, endpoint.basicAuth, tls) + EsploraClient(endpoint.url, endpoint.basicAuth, tls, endpoint.bearerAuth) } val extractBlock = ExtractBlock() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/GenericConnectorFactoryCreator.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/GenericConnectorFactoryCreator.kt index ee46c29c..fa2a650b 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/GenericConnectorFactoryCreator.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/configure/GenericConnectorFactoryCreator.kt @@ -84,6 +84,7 @@ open class GenericConnectorFactoryCreator( monitoringCfg.nettyMetricsConfig.enabled, httpScheduler, customHeaders, + conn.bearerAuth, ) } } @@ -106,6 +107,7 @@ open class GenericConnectorFactoryCreator( ).apply { config = endpoint basicAuth = endpoint.basicAuth + bearerAuth = endpoint.bearerAuth this.customHeaders = customHeaders } val wsApi = WsConnectionPoolFactory( diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/BasicHttpFactory.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/BasicHttpFactory.kt index 38abb0bb..259e8360 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/BasicHttpFactory.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/BasicHttpFactory.kt @@ -21,6 +21,7 @@ class BasicHttpFactory( private val nettyMetricsEnabled: Boolean, private val httpScheduler: Scheduler, private val customHeaders: Map = emptyMap(), + private val bearerAuth: AuthConfig.ClientBearerAuth? = null, ) : HttpFactory { private val log = LoggerFactory.getLogger(this::class.java) @@ -50,8 +51,8 @@ class BasicHttpFactory( ) if (chain.type.apiType == ApiType.REST) { - return RestHttpReader(url, maxConnections, queueSize, metrics, httpScheduler, chain, basicAuth, tls, customHeaders) + return RestHttpReader(url, maxConnections, queueSize, metrics, httpScheduler, chain, basicAuth, tls, customHeaders, bearerAuth) } - return JsonRpcHttpReader(url, maxConnections, queueSize, metrics, httpScheduler, basicAuth, tls, customHeaders) + return JsonRpcHttpReader(url, maxConnections, queueSize, metrics, httpScheduler, basicAuth, tls, customHeaders, bearerAuth) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/HttpReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/HttpReader.kt index 28e19d75..818a96d2 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/HttpReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/HttpReader.kt @@ -28,6 +28,7 @@ abstract class HttpReader( basicAuth: AuthConfig.ClientBasicAuth? = null, tlsCAAuth: ByteArray? = null, customHeaders: Map = emptyMap(), + bearerAuth: AuthConfig.ClientBearerAuth? = null, ) : ChainReader { constructor() : this("", 1500, 1000, null) @@ -66,6 +67,13 @@ abstract class HttpReader( build = build.headers(headers) } + if (basicAuth == null) { + bearerAuth?.let { auth -> + val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, "Bearer ${auth.token}") } + build = build.headers(headers) + } + } + if (customHeaders.isNotEmpty()) { val headers = Consumer { h: HttpHeaders -> customHeaders.forEach { (key, value) -> diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/EsploraClient.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/EsploraClient.kt index eb9709d7..7c5ad1a6 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/EsploraClient.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/EsploraClient.kt @@ -34,10 +34,11 @@ import java.security.cert.X509Certificate import java.util.Base64 import java.util.function.Consumer -class EsploraClient( +class EsploraClient @JvmOverloads constructor( private val url: URI, basicAuth: AuthConfig.ClientBasicAuth? = null, tlsCAAuth: ByteArray? = null, + bearerAuth: AuthConfig.ClientBearerAuth? = null, ) { companion object { @@ -62,6 +63,13 @@ class EsploraClient( build = build.headers(headers) } + if (basicAuth == null) { + bearerAuth?.let { auth -> + val headers = Consumer { h: HttpHeaders -> h.add(HttpHeaderNames.AUTHORIZATION, "Bearer ${auth.token}") } + build = build.headers(headers) + } + } + tlsCAAuth?.let { auth -> val cf = CertificateFactory.getInstance("X.509") val cert = cf.generateCertificate(ByteArrayInputStream(auth)) as X509Certificate diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionFactory.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionFactory.kt index 526950bd..87bc8465 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionFactory.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionFactory.kt @@ -21,6 +21,7 @@ open class WsConnectionFactory( ) { var basicAuth: AuthConfig.ClientBasicAuth? = null + var bearerAuth: AuthConfig.ClientBearerAuth? = null var config: UpstreamsConfig.WsEndpoint? = null var customHeaders: Map = emptyMap() @@ -47,7 +48,7 @@ open class WsConnectionFactory( } open fun createWsConnection(connIndex: Int = 0): WsConnection = - WsConnectionImpl(uri, origin, basicAuth, metrics(connIndex), scheduler, eventsScheduler, customHeaders).also { ws -> + WsConnectionImpl(uri, origin, basicAuth, metrics(connIndex), scheduler, eventsScheduler, customHeaders, bearerAuth).also { ws -> config?.frameSize?.let { ws.frameSize = it } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionImpl.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionImpl.kt index a7ec18e9..23889656 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionImpl.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsConnectionImpl.kt @@ -67,6 +67,7 @@ open class WsConnectionImpl( private val scheduler: Scheduler, private val eventsScheduler: Scheduler, private val customHeaders: Map = emptyMap(), + private val bearerAuth: AuthConfig.ClientBearerAuth? = null, ) : AutoCloseable, WsConnection, Cloneable { companion object { @@ -227,6 +228,11 @@ open class WsConnectionImpl( val base64password = Base64.getEncoder().encodeToString(tmp.toByteArray()) headers.add(HttpHeaderNames.AUTHORIZATION, "Basic $base64password") } + if (basicAuth == null) { + bearerAuth?.let { auth -> + headers.add(HttpHeaderNames.AUTHORIZATION, "Bearer ${auth.token}") + } + } customHeaders.forEach { (key, value) -> headers.add(key, value) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt index aa0b2da7..ee83d6e7 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt @@ -36,7 +36,8 @@ class RestHttpReader( basicAuth: AuthConfig.ClientBasicAuth? = null, tlsCAAuth: ByteArray? = null, customHeaders: Map = emptyMap(), -) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth, customHeaders) { + bearerAuth: AuthConfig.ClientBearerAuth? = null, +) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth, customHeaders, bearerAuth) { private val parser = ResponseRpcParser() private val requestParser = RestRequestParser diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReader.kt index 6fc78e52..e3e58b0c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReader.kt @@ -36,7 +36,7 @@ import java.util.function.Function /** * JSON RPC client */ -class JsonRpcHttpReader( +class JsonRpcHttpReader @JvmOverloads constructor( target: String, maxConnections: Int, queueSize: Int, @@ -45,7 +45,8 @@ class JsonRpcHttpReader( basicAuth: AuthConfig.ClientBasicAuth? = null, tlsCAAuth: ByteArray? = null, customHeaders: Map = emptyMap(), -) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth, customHeaders) { + bearerAuth: AuthConfig.ClientBearerAuth? = null, +) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth, customHeaders, bearerAuth) { private val parser = ResponseRpcParser() private val streamParser = JsonRpcStreamParser() diff --git a/src/test/groovy/io/emeraldpay/dshackle/config/AuthConfigReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/config/AuthConfigReaderSpec.groovy index c31a95a9..51593497 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/config/AuthConfigReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/config/AuthConfigReaderSpec.groovy @@ -35,6 +35,29 @@ class AuthConfigReaderSpec extends Specification { act.password == "258fe4149c199ad8f2811a68f20154fc" } + def "Read bearer-auth for client"() { + setup: + def yaml = + "bearer-auth:\n" + + " token: 9c199ad8f281f20154fc258fe41a6814" + when: + def act = reader.readClientBearerAuth(reader.readNode(yaml)) + then: + act != null + act.token == "9c199ad8f281f20154fc258fe41a6814" + } + + def "Read bearer-auth without token as null"() { + setup: + def yaml = + "bearer-auth:\n" + + " something: else" + when: + def act = reader.readClientBearerAuth(reader.readNode(yaml)) + then: + act == null + } + def "Read tls for client"() { setup: def yaml = diff --git a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy index 58c15109..d0824fff 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/config/UpstreamsConfigReaderSpec.groovy @@ -75,6 +75,38 @@ class UpstreamsConfigReaderSpec extends Specification { } } + def "Parse config with bearer auth"() { + setup: + def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-bearer-auth.yaml") + when: + def act = reader.readInternal(config) + then: + act != null + act.upstreams.size() == 2 + with(act.upstreams.get(0)) { + id == "local" + connection instanceof UpstreamsConfig.RpcConnection + with((UpstreamsConfig.RpcConnection) connection) { + rpc.basicAuth == null + rpc.bearerAuth != null + rpc.bearerAuth.token == "9c199ad8f281f20154fc258fe41a6814" + ws != null + ws.basicAuth == null + ws.bearerAuth != null + ws.bearerAuth.token == "258fe4149c199ad8f2811a68f20154fc" + } + } + with(act.upstreams.get(1)) { + id == "both-auth" + connection instanceof UpstreamsConfig.RpcConnection + with((UpstreamsConfig.RpcConnection) connection) { + // when both are configured basic-auth wins and bearer-auth is dropped + rpc.basicAuth != null + rpc.bearerAuth == null + } + } + } + def "Parse websocket-only config"() { setup: def config = this.class.getClassLoader().getResourceAsStream("configs/upstreams-ws-only.yaml") diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReaderSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReaderSpec.groovy index 3ad63420..588a3e54 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReaderSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcHttpReaderSpec.groovy @@ -16,6 +16,7 @@ package io.emeraldpay.dshackle.upstream.rpcclient +import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.upstream.ChainException import io.emeraldpay.dshackle.upstream.ChainRequest @@ -69,6 +70,28 @@ class JsonRpcHttpReaderSpec extends Specification { new String(act.result) == '"0x98de45"' } + def "Make a request with bearer auth"() { + setup: + def bearerAuth = new AuthConfig.ClientBearerAuth("test-token-123") + JsonRpcHttpReader client = new JsonRpcHttpReader("localhost:${mockServer.port}", 50, 50, metrics, Schedulers.boundedElastic(), null, null, [:], bearerAuth) + def resp = '{' + + ' "jsonrpc": "2.0",' + + ' "result": "0x98de45",' + + ' "error": null,' + + ' "id": 15' + + '}' + mockServer.when( + HttpRequest.request().withHeader("authorization", "Bearer test-token-123") + ).respond( + HttpResponse.response(resp) + ) + when: + def act = client.read(new ChainRequest("test", new ListParams())).block(Duration.ofSeconds(5)) + then: + act.error == null + new String(act.result) == '"0x98de45"' + } + def "Produces RPC Exception on error status code"() { setup: def client = new JsonRpcHttpReader("localhost:${mockServer.port}", 50, 50, metrics, Schedulers.boundedElastic(), null, null, [:]) diff --git a/src/test/resources/configs/upstreams-bearer-auth.yaml b/src/test/resources/configs/upstreams-bearer-auth.yaml new file mode 100644 index 00000000..b9ad19d2 --- /dev/null +++ b/src/test/resources/configs/upstreams-bearer-auth.yaml @@ -0,0 +1,27 @@ +version: v1 + +upstreams: + - id: local + chain: ethereum + connection: + ethereum: + rpc: + url: "http://localhost:8545" + bearer-auth: + token: 9c199ad8f281f20154fc258fe41a6814 + ws: + url: "ws://localhost:8546" + origin: "http://localhost" + bearer-auth: + token: 258fe4149c199ad8f2811a68f20154fc + - id: both-auth + chain: ethereum + connection: + ethereum: + rpc: + url: "http://localhost:8545" + basic-auth: + username: 4fc258fe41a68149c199ad8f281f2015 + password: 1a68f20154fc258fe4149c199ad8f281 + bearer-auth: + token: 9c199ad8f281f20154fc258fe41a6814