solution: allow TLS configuration for proxy
This commit is contained in:
@@ -34,7 +34,8 @@ class ProxyStarter(
|
||||
@Autowired private val env: Environment,
|
||||
@Autowired private val readRpcJson: ReadRpcJson,
|
||||
@Autowired private val writeRpcJson: WriteRpcJson,
|
||||
@Autowired private val nativeCall: NativeCall
|
||||
@Autowired private val nativeCall: NativeCall,
|
||||
@Autowired private val tlsSetup: TlsSetup
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -48,7 +49,7 @@ class ProxyStarter(
|
||||
log.debug("Proxy server is not configured")
|
||||
return
|
||||
}
|
||||
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall)
|
||||
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall, tlsSetup)
|
||||
server.start()
|
||||
}
|
||||
|
||||
|
||||
83
src/main/kotlin/io/emeraldpay/dshackle/TlsSetup.kt
Normal file
83
src/main/kotlin/io/emeraldpay/dshackle/TlsSetup.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2020 ETCDEV GmbH
|
||||
*
|
||||
* 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
|
||||
|
||||
import io.emeraldpay.dshackle.config.AuthConfig
|
||||
import io.grpc.netty.GrpcSslContexts
|
||||
import io.netty.handler.ssl.ClientAuth
|
||||
import io.netty.handler.ssl.SslContext
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class TlsSetup(
|
||||
@Autowired val fileResolver: FileResolver
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TlsSetup::class.java)
|
||||
}
|
||||
|
||||
fun setupServer(category: String, config: AuthConfig.ServerTlsAuth): SslContext? {
|
||||
val mustBeSecure = config.enabled != null && config.enabled!!
|
||||
val tlsDisabled = config.enabled != null && !config.enabled!!
|
||||
var hasServerCertificate = true
|
||||
if (!tlsDisabled) {
|
||||
if (StringUtils.isEmpty(config.certificate)) {
|
||||
if (mustBeSecure) {
|
||||
log.error("tls.server.certificate property for $category is not set (path to server TLS certificate) but TLS is enabled")
|
||||
throw IllegalArgumentException("Certificate not set")
|
||||
}
|
||||
hasServerCertificate = false
|
||||
}
|
||||
if (StringUtils.isEmpty(config.key)) {
|
||||
if (mustBeSecure) {
|
||||
log.error("tls.server.key property for $category is not set (path to server TLS certificate key) but TLS is enabled")
|
||||
throw IllegalArgumentException("Certificate Key not set")
|
||||
}
|
||||
hasServerCertificate = false
|
||||
}
|
||||
}
|
||||
if (mustBeSecure || (!tlsDisabled && hasServerCertificate)) {
|
||||
log.info("Using TLS for $category")
|
||||
val sslContextBuilder = SslContextBuilder.forServer(
|
||||
fileResolver.resolve(config.certificate!!),
|
||||
fileResolver.resolve(config.key!!)
|
||||
)
|
||||
if (StringUtils.isNotEmpty(config.clientCa)) {
|
||||
log.info("Using TLS for client authentication for $category")
|
||||
sslContextBuilder.trustManager(
|
||||
fileResolver.resolve(config.clientCa!!)
|
||||
)
|
||||
if (config.clientRequire != null && config.clientRequire!!) {
|
||||
sslContextBuilder.clientAuth(ClientAuth.REQUIRE)
|
||||
}
|
||||
} else if (config.clientRequire != null && config.clientRequire!!) {
|
||||
throw IllegalArgumentException("Client Certificate not set")
|
||||
} else {
|
||||
log.warn("Trust all clients for $category")
|
||||
}
|
||||
return sslContextBuilder.build()
|
||||
} else {
|
||||
log.warn("Using insecure transport for $category")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
60
src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfig.kt
Normal file
60
src/main/kotlin/io/emeraldpay/dshackle/config/AuthConfig.kt
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2020 ETCDEV GmbH
|
||||
*
|
||||
* 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.config
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class AuthConfig {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AuthConfig::class.java)
|
||||
}
|
||||
|
||||
open class ClientAuth {
|
||||
var type: String? = null
|
||||
}
|
||||
|
||||
class ClientBasicAuth(
|
||||
val username: String,
|
||||
val password: String
|
||||
) : ClientAuth()
|
||||
|
||||
class ClientTlsAuth(
|
||||
var ca: String? = null,
|
||||
var certificate: String? = null,
|
||||
var key: String? = null
|
||||
) : ClientAuth()
|
||||
|
||||
/**
|
||||
* Example config:
|
||||
* ```
|
||||
* enabled: false
|
||||
* server:
|
||||
* certificate: "127.0.0.1.crt"
|
||||
* key: "127.0.0.1.p8.key"
|
||||
* client:
|
||||
* require: false
|
||||
* ca: "ca.dshackle.test.crt"
|
||||
* ```
|
||||
*/
|
||||
open class ServerTlsAuth {
|
||||
var enabled: Boolean? = null
|
||||
var certificate: String? = null
|
||||
var key: String? = null
|
||||
var clientRequire: Boolean? = null
|
||||
var clientCa: String? = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2020 ETCDEV GmbH
|
||||
*
|
||||
* 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.config
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
|
||||
class AuthConfigReader : YamlConfigReader() {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(AuthConfigReader::class.java)
|
||||
}
|
||||
|
||||
fun readClientBasicAuth(node: MappingNode?): AuthConfig.ClientBasicAuth? {
|
||||
return getMapping(node, "basic-auth")?.let { authNode ->
|
||||
val username = getValueAsString(authNode, "username")
|
||||
val password = getValueAsString(authNode, "password")
|
||||
if (username != null && password != null) {
|
||||
AuthConfig.ClientBasicAuth(username, password)
|
||||
} else {
|
||||
log.warn("Basic auth is not fully configured")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readClientTls(node: MappingNode?): AuthConfig.ClientTlsAuth? {
|
||||
return getMapping(node, "tls")?.let { authNode ->
|
||||
val auth = AuthConfig.ClientTlsAuth()
|
||||
auth.ca = getValueAsString(authNode, "ca")
|
||||
auth.certificate = getValueAsString(authNode, "certificate")
|
||||
auth.key = getValueAsString(authNode, "key")
|
||||
auth
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example config:
|
||||
* ```
|
||||
* enabled: false
|
||||
* server:
|
||||
* certificate: "127.0.0.1.crt"
|
||||
* key: "127.0.0.1.p8.key"
|
||||
* client:
|
||||
* require: false
|
||||
* ca: "ca.dshackle.test.crt"
|
||||
* ```
|
||||
*/
|
||||
fun readServerTls(node: MappingNode?): AuthConfig.ServerTlsAuth? {
|
||||
return getMapping(node, "tls")?.let { node ->
|
||||
val auth = AuthConfig.ServerTlsAuth()
|
||||
getValueAsBool(node, "enabled")?.let {
|
||||
auth.enabled = it
|
||||
}
|
||||
getMapping(node, "server")?.let { node ->
|
||||
auth.certificate = getValueAsString(node, "certificate")
|
||||
auth.key = getValueAsString(node, "key")
|
||||
}
|
||||
getMapping(node, "client")?.let { node ->
|
||||
getValueAsBool(node, "require")?.let {
|
||||
auth.clientRequire = it
|
||||
}
|
||||
auth.clientCa = getValueAsString(node, "ca")
|
||||
}
|
||||
auth
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,6 +38,11 @@ class ProxyConfig {
|
||||
*/
|
||||
var port: Int = 8080
|
||||
|
||||
/**
|
||||
* TLS Auth required from clients.
|
||||
*/
|
||||
var tls: AuthConfig.ServerTlsAuth? = null
|
||||
|
||||
/**
|
||||
* List of available routes
|
||||
*/
|
||||
|
||||
@@ -33,10 +33,10 @@ class ProxyConfigReader : YamlConfigReader() {
|
||||
}
|
||||
|
||||
private var filename = "dshackle.yaml"
|
||||
private val authConfigReader = AuthConfigReader()
|
||||
|
||||
fun read(input: InputStream): ProxyConfig? {
|
||||
val yaml = Yaml()
|
||||
val configNode = asMappingNode(yaml.compose(InputStreamReader(input)))
|
||||
val configNode = readNode(input)
|
||||
return read(getMapping(configNode, "proxy"))
|
||||
}
|
||||
|
||||
@@ -73,8 +73,10 @@ class ProxyConfigReader : YamlConfigReader() {
|
||||
}
|
||||
}
|
||||
if (config.routes.isEmpty()) {
|
||||
log.warn("Proxy config has no routes")
|
||||
return null
|
||||
}
|
||||
config.tls = authConfigReader.readServerTls(input)
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ package io.emeraldpay.dshackle.config
|
||||
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
@@ -81,7 +80,7 @@ class UpstreamsConfig {
|
||||
class GrpcConnection : UpstreamConnection() {
|
||||
var host: String? = null
|
||||
var port: Int = 0
|
||||
var auth: TlsAuth? = null
|
||||
var auth: AuthConfig.ClientTlsAuth? = null
|
||||
}
|
||||
|
||||
class EthereumConnection : UpstreamConnection() {
|
||||
@@ -90,29 +89,16 @@ class UpstreamsConfig {
|
||||
}
|
||||
|
||||
class HttpEndpoint(val url: URI) {
|
||||
var basicAuth: BasicAuth? = null
|
||||
var tls: TlsAuth? = null
|
||||
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
||||
var tls: AuthConfig.ClientTlsAuth? = null
|
||||
}
|
||||
|
||||
class WsEndpoint(val url: URI) {
|
||||
var origin: URI? = null
|
||||
var basicAuth: BasicAuth? = null
|
||||
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
||||
}
|
||||
|
||||
open class Auth {
|
||||
var type: String? = null
|
||||
}
|
||||
|
||||
class BasicAuth(
|
||||
val username: String,
|
||||
val password: String
|
||||
) : Auth()
|
||||
|
||||
class TlsAuth(
|
||||
var ca: String? = null,
|
||||
var certificate: String? = null,
|
||||
var key: String? = null
|
||||
) : Auth()
|
||||
|
||||
//TODO make it unmodifiable after initial load
|
||||
class Labels: HashMap<String, String>() {
|
||||
|
||||
@@ -18,24 +18,21 @@ package io.emeraldpay.dshackle.config
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import org.yaml.snakeyaml.nodes.CollectionNode
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import org.yaml.snakeyaml.nodes.Node
|
||||
import org.yaml.snakeyaml.nodes.ScalarNode
|
||||
import reactor.util.function.Tuples
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
|
||||
class UpstreamsConfigReader : YamlConfigReader() {
|
||||
|
||||
private val log = LoggerFactory.getLogger(UpstreamsConfigReader::class.java)
|
||||
private val authConfigReader = AuthConfigReader()
|
||||
|
||||
fun read(input: InputStream): UpstreamsConfig {
|
||||
val yaml = Yaml()
|
||||
val configNode = asMappingNode(yaml.compose(InputStreamReader(input)))
|
||||
val configNode = readNode(input)
|
||||
|
||||
val config = UpstreamsConfig()
|
||||
config.version = getValueAsString(configNode, "version")
|
||||
@@ -65,8 +62,8 @@ class UpstreamsConfigReader : YamlConfigReader() {
|
||||
getValueAsString(node, "url")?.let { url ->
|
||||
val http = UpstreamsConfig.HttpEndpoint(URI(url))
|
||||
connection.rpc = http
|
||||
http.basicAuth = readBasicAuth(node)
|
||||
http.tls = readTls(node)
|
||||
http.basicAuth = authConfigReader.readClientBasicAuth(node)
|
||||
http.tls = authConfigReader.readClientTls(node)
|
||||
}
|
||||
}
|
||||
getMapping(connConfigNode, "ws")?.let { node ->
|
||||
@@ -76,7 +73,7 @@ class UpstreamsConfigReader : YamlConfigReader() {
|
||||
getValueAsString(node, "origin")?.let { origin ->
|
||||
ws.origin = URI(origin)
|
||||
}
|
||||
ws.basicAuth = readBasicAuth(node)
|
||||
ws.basicAuth = authConfigReader.readClientBasicAuth(node)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -97,7 +94,7 @@ class UpstreamsConfigReader : YamlConfigReader() {
|
||||
getValueAsInt(connConfigNode, "port")?.let {
|
||||
connection.port = it
|
||||
}
|
||||
connection.auth = readTls(connConfigNode)
|
||||
connection.auth = authConfigReader.readClientTls(connConfigNode)
|
||||
} else {
|
||||
log.error("Upstream at #0 has invalid configuration")
|
||||
}
|
||||
@@ -194,27 +191,4 @@ class UpstreamsConfigReader : YamlConfigReader() {
|
||||
return options
|
||||
}
|
||||
|
||||
private fun readBasicAuth(node: MappingNode?): UpstreamsConfig.BasicAuth? {
|
||||
return getMapping(node, "basic-auth")?.let { authNode ->
|
||||
val username = getValueAsString(authNode, "username")
|
||||
val password = getValueAsString(authNode, "password")
|
||||
if (username != null && password != null) {
|
||||
UpstreamsConfig.BasicAuth(username, password)
|
||||
} else {
|
||||
log.warn("Basic auth is not fully configured")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readTls(node: MappingNode?): UpstreamsConfig.TlsAuth? {
|
||||
return getMapping(node, "tls")?.let { authNode ->
|
||||
val auth = UpstreamsConfig.TlsAuth()
|
||||
auth.ca = getValueAsString(authNode, "ca")
|
||||
auth.certificate = getValueAsString(authNode, "certificate")
|
||||
auth.key = getValueAsString(authNode, "key")
|
||||
auth
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,14 +16,26 @@
|
||||
package io.emeraldpay.dshackle.config
|
||||
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import org.yaml.snakeyaml.nodes.CollectionNode
|
||||
import org.yaml.snakeyaml.nodes.MappingNode
|
||||
import org.yaml.snakeyaml.nodes.Node
|
||||
import org.yaml.snakeyaml.nodes.ScalarNode
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
|
||||
open class YamlConfigReader {
|
||||
private val envVariables = EnvVariables()
|
||||
|
||||
fun readNode(input: String): MappingNode {
|
||||
return readNode(input.byteInputStream())
|
||||
}
|
||||
|
||||
fun readNode(input: InputStream): MappingNode {
|
||||
val yaml = Yaml()
|
||||
return asMappingNode(yaml.compose(InputStreamReader(input)))
|
||||
}
|
||||
|
||||
protected fun hasAny(mappingNode: MappingNode?, key: String): Boolean {
|
||||
if (mappingNode == null) {
|
||||
return false
|
||||
|
||||
@@ -17,9 +17,11 @@ package io.emeraldpay.dshackle.proxy
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.TlsSetup
|
||||
import io.emeraldpay.dshackle.config.ProxyConfig
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import org.reactivestreams.Publisher
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
@@ -29,6 +31,7 @@ import reactor.netty.http.server.HttpServer
|
||||
import reactor.netty.http.server.HttpServerRequest
|
||||
import reactor.netty.http.server.HttpServerResponse
|
||||
import reactor.netty.http.server.HttpServerRoutes
|
||||
import java.io.File
|
||||
import java.util.function.BiFunction
|
||||
|
||||
/**
|
||||
@@ -38,7 +41,8 @@ class ProxyServer(
|
||||
private var config: ProxyConfig,
|
||||
private val readRpcJson: ReadRpcJson,
|
||||
private val writeRpcJson: WriteRpcJson,
|
||||
private val nativeCall: NativeCall
|
||||
private val nativeCall: NativeCall,
|
||||
private val tlsSetup: TlsSetup
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -51,9 +55,17 @@ class ProxyServer(
|
||||
return
|
||||
}
|
||||
log.info("Listening Proxy on ${config.host}:${config.port}")
|
||||
val server: DisposableServer = HttpServer.create()
|
||||
var serverBuilder = HttpServer.create()
|
||||
.host(config.host)
|
||||
.port(config.port)
|
||||
|
||||
config.tls?.let { tls ->
|
||||
tlsSetup.setupServer("proxy", tls)?.let { sslContext ->
|
||||
serverBuilder = serverBuilder.secure { secure -> secure.sslContext(sslContext) }
|
||||
}
|
||||
}
|
||||
|
||||
val server: DisposableServer = serverBuilder
|
||||
.route(this::setupRoutes)
|
||||
.bindNow()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.cache.Caches
|
||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||
import io.emeraldpay.dshackle.config.AuthConfig
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.reader.EmptyReader
|
||||
import io.emeraldpay.dshackle.reader.Reader
|
||||
@@ -45,7 +46,7 @@ class EthereumWs(
|
||||
.builder<BlockJson<TransactionRefJson>>()
|
||||
.name("new-blocks")
|
||||
.build()
|
||||
var basicAuth: UpstreamsConfig.BasicAuth? = null
|
||||
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
||||
|
||||
private var blockCache: Reader<BlockHash, BlockJson<TransactionRefJson>> = EmptyReader()
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||
import io.emeraldpay.dshackle.Defaults
|
||||
import io.emeraldpay.dshackle.FileResolver
|
||||
import io.emeraldpay.dshackle.config.AuthConfig
|
||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||
import io.emeraldpay.dshackle.startup.UpstreamChange
|
||||
@@ -45,7 +46,7 @@ class GrpcUpstreams(
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val auth: UpstreamsConfig.TlsAuth? = null,
|
||||
private val auth: AuthConfig.ClientTlsAuth? = null,
|
||||
private val fileResolver: FileResolver
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
|
||||
@@ -133,7 +134,7 @@ class GrpcUpstreams(
|
||||
return Flux.fromIterable(removed + added)
|
||||
}
|
||||
|
||||
internal fun withTls(auth: UpstreamsConfig.TlsAuth): SslContext {
|
||||
internal fun withTls(auth: AuthConfig.ClientTlsAuth): SslContext {
|
||||
val sslContext = SslContextBuilder.forClient()
|
||||
.clientAuth(ClientAuth.REQUIRE)
|
||||
sslContext.trustManager(fileResolver.resolve(auth.ca!!).inputStream())
|
||||
|
||||
Reference in New Issue
Block a user