mirror of
https://github.com/corda/corda.git
synced 2024-12-24 15:16:45 +00:00
CORDA-2113 - Include PNM ID in CSR (#4086)
* CORDA-2113 - Include PNM ID in CSR If Compatibility Zone operator is using private networks and the node should be joining one, optionally the ID (a UUID) of that network can be included as part of the node's CSR to to the Doorman. * fix broken test
This commit is contained in:
parent
8af404427f
commit
7cfd44e383
@ -194,6 +194,7 @@ absolute path to the node's base directory.
|
|||||||
|
|
||||||
:doormanURL: Root address of the network registration service.
|
:doormanURL: Root address of the network registration service.
|
||||||
:networkMapURL: Root address of the network map service.
|
:networkMapURL: Root address of the network map service.
|
||||||
|
:pnm: Optional UUID of the private network operating within the compatibility zone this node should be joinging.
|
||||||
|
|
||||||
.. note:: Only one of ``compatibilityZoneURL`` or ``networkServices`` should be used.
|
.. note:: Only one of ``compatibilityZoneURL`` or ``networkServices`` should be used.
|
||||||
|
|
||||||
|
@ -47,18 +47,22 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Parameterized.Parameters(name = "{0}")
|
@Parameterized.Parameters(name = "{0}")
|
||||||
fun runParams() = listOf(
|
fun runParams() = listOf(
|
||||||
{ addr: URL, nms: NetworkMapServer ->
|
{
|
||||||
SharedCompatibilityZoneParams(
|
addr: URL,
|
||||||
|
nms: NetworkMapServer -> SharedCompatibilityZoneParams(
|
||||||
addr,
|
addr,
|
||||||
|
pnm = null,
|
||||||
publishNotaries = {
|
publishNotaries = {
|
||||||
nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2)
|
nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ addr: URL, nms: NetworkMapServer ->
|
{
|
||||||
SplitCompatibilityZoneParams(
|
addr: URL,
|
||||||
|
nms: NetworkMapServer -> SplitCompatibilityZoneParams (
|
||||||
doormanURL = URL("http://I/Don't/Exist"),
|
doormanURL = URL("http://I/Don't/Exist"),
|
||||||
networkMapURL = addr,
|
networkMapURL = addr,
|
||||||
|
pnm = null,
|
||||||
publishNotaries = {
|
publishNotaries = {
|
||||||
nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2)
|
nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2)
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,7 @@ class NodeRegistrationTest {
|
|||||||
fun `node registration correct root cert`() {
|
fun `node registration correct root cert`() {
|
||||||
val compatibilityZone = SharedCompatibilityZoneParams(
|
val compatibilityZone = SharedCompatibilityZoneParams(
|
||||||
URL("http://$serverHostAndPort"),
|
URL("http://$serverHostAndPort"),
|
||||||
|
null,
|
||||||
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
|
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
|
||||||
rootCert = DEV_ROOT_CA.certificate)
|
rootCert = DEV_ROOT_CA.certificate)
|
||||||
internalDriver(
|
internalDriver(
|
||||||
|
@ -152,7 +152,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
|||||||
|
|
||||||
private val handleRegistrationError = { error: Exception ->
|
private val handleRegistrationError = { error: Exception ->
|
||||||
when (error) {
|
when (error) {
|
||||||
is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.")
|
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
|
||||||
else -> error.logAsUnexpected("Exception during node registration")
|
else -> error.logAsUnexpected("Exception during node registration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -385,17 +385,23 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
|||||||
logger.info(nodeStartedMessage)
|
logger.info(nodeStartedMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun registerWithNetwork(conf: NodeConfiguration, versionInfo: VersionInfo, nodeRegistrationConfig: NodeRegistrationOption) {
|
protected open fun registerWithNetwork(
|
||||||
val compatibilityZoneURL = conf.networkServices?.doormanURL ?: throw RuntimeException(
|
conf: NodeConfiguration,
|
||||||
"compatibilityZoneURL or networkServices must be configured!")
|
versionInfo: VersionInfo,
|
||||||
|
nodeRegistrationConfig: NodeRegistrationOption
|
||||||
|
) {
|
||||||
|
println("\n" +
|
||||||
|
"******************************************************************\n" +
|
||||||
|
"* *\n" +
|
||||||
|
"* Registering as a new participant with a Corda network *\n" +
|
||||||
|
"* *\n" +
|
||||||
|
"******************************************************************\n")
|
||||||
|
|
||||||
println()
|
NodeRegistrationHelper(conf,
|
||||||
println("******************************************************************")
|
HTTPNetworkRegistrationService(
|
||||||
println("* *")
|
requireNotNull(conf.networkServices),
|
||||||
println("* Registering as a new participant with Corda network *")
|
versionInfo),
|
||||||
println("* *")
|
nodeRegistrationConfig).buildKeystore()
|
||||||
println("******************************************************************")
|
|
||||||
NodeRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL, versionInfo), nodeRegistrationConfig).buildKeystore()
|
|
||||||
|
|
||||||
// Minimal changes to make registration tool create node identity.
|
// Minimal changes to make registration tool create node identity.
|
||||||
// TODO: Move node identity generation logic from node to registration helper.
|
// TODO: Move node identity generation logic from node to registration helper.
|
||||||
|
@ -156,6 +156,8 @@ data class BFTSMaRtConfiguration(
|
|||||||
*
|
*
|
||||||
* @property doormanURL The URL of the tls certificate signing service.
|
* @property doormanURL The URL of the tls certificate signing service.
|
||||||
* @property networkMapURL The URL of the Network Map service.
|
* @property networkMapURL The URL of the Network Map service.
|
||||||
|
* @property pnm If the compatibility zone operator supports the private network map option, have the node
|
||||||
|
* at registration automatically join that private network.
|
||||||
* @property inferred Non user setting that indicates weather the Network Services configuration was
|
* @property inferred Non user setting that indicates weather the Network Services configuration was
|
||||||
* set explicitly ([inferred] == false) or weather they have been inferred via the compatibilityZoneURL parameter
|
* set explicitly ([inferred] == false) or weather they have been inferred via the compatibilityZoneURL parameter
|
||||||
* ([inferred] == true) where both the network map and doorman are running on the same endpoint. Only one,
|
* ([inferred] == true) where both the network map and doorman are running on the same endpoint. Only one,
|
||||||
@ -164,6 +166,7 @@ data class BFTSMaRtConfiguration(
|
|||||||
data class NetworkServicesConfig(
|
data class NetworkServicesConfig(
|
||||||
val doormanURL: URL,
|
val doormanURL: URL,
|
||||||
val networkMapURL: URL,
|
val networkMapURL: URL,
|
||||||
|
val pnm: UUID? = null,
|
||||||
val inferred : Boolean = false
|
val inferred : Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -371,8 +374,9 @@ data class NodeConfigurationImpl(
|
|||||||
""".trimMargin())
|
""".trimMargin())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support the deprecated method of configuring network services with a single compatibilityZoneURL option
|
||||||
if (compatibilityZoneURL != null && networkServices == null) {
|
if (compatibilityZoneURL != null && networkServices == null) {
|
||||||
networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, true)
|
networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, inferred = true)
|
||||||
}
|
}
|
||||||
require(h2port == null || h2Settings == null) { "Cannot specify both 'h2port' and 'h2Settings' in configuration" }
|
require(h2port == null || h2Settings == null) { "Cannot specify both 'h2port' and 'h2Settings' in configuration" }
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.internal.post
|
|||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.VersionInfo
|
import net.corda.node.VersionInfo
|
||||||
|
import net.corda.node.services.config.NetworkServicesConfig
|
||||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
@ -19,8 +20,11 @@ import java.util.*
|
|||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import javax.naming.ServiceUnavailableException
|
import javax.naming.ServiceUnavailableException
|
||||||
|
|
||||||
class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: VersionInfo) : NetworkRegistrationService {
|
class HTTPNetworkRegistrationService(
|
||||||
private val registrationURL = URL("$compatibilityZoneURL/certificate")
|
val config : NetworkServicesConfig,
|
||||||
|
val versionInfo: VersionInfo
|
||||||
|
) : NetworkRegistrationService {
|
||||||
|
private val registrationURL = URL("${config.doormanURL}/certificate")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TRANSIENT_ERROR_STATUS_CODES = setOf(HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT)
|
private val TRANSIENT_ERROR_STATUS_CODES = setOf(HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT)
|
||||||
@ -54,7 +58,8 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo:
|
|||||||
override fun submitRequest(request: PKCS10CertificationRequest): String {
|
override fun submitRequest(request: PKCS10CertificationRequest): String {
|
||||||
return String(registrationURL.post(OpaqueBytes(request.encoded),
|
return String(registrationURL.post(OpaqueBytes(request.encoded),
|
||||||
"Platform-Version" to "${versionInfo.platformVersion}",
|
"Platform-Version" to "${versionInfo.platformVersion}",
|
||||||
"Client-Version" to versionInfo.releaseVersion))
|
"Client-Version" to versionInfo.releaseVersion,
|
||||||
|
"Private-Network-Map" to (config.pnm?.toString() ?: "")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,10 +96,9 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
|||||||
val requestId = try {
|
val requestId = try {
|
||||||
submitOrResumeCertificateSigningRequest(keyPair)
|
submitOrResumeCertificateSigningRequest(keyPair)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is ConnectException || e is ServiceUnavailableException || e is IOException) {
|
throw if (e is ConnectException || e is ServiceUnavailableException || e is IOException) {
|
||||||
throw NodeRegistrationException(e)
|
NodeRegistrationException(e.message, e)
|
||||||
}
|
} else e
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val certificates = try {
|
val certificates = try {
|
||||||
@ -200,7 +199,8 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
|||||||
if (idlePeriodDuration != null) {
|
if (idlePeriodDuration != null) {
|
||||||
Thread.sleep(idlePeriodDuration.toMillis())
|
Thread.sleep(idlePeriodDuration.toMillis())
|
||||||
} else {
|
} else {
|
||||||
throw NodeRegistrationException(e)
|
throw NodeRegistrationException("Compatibility Zone registration service is currently unavailable, "
|
||||||
|
+ "try again later!.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -249,10 +249,17 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
|||||||
protected open fun isTlsCrlIssuerCertRequired(): Boolean = false
|
protected open fun isTlsCrlIssuerCertRequired(): Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeRegistrationException(cause: Throwable?) : IOException("Unable to contact node registration service", cause)
|
class NodeRegistrationException(
|
||||||
|
message: String?,
|
||||||
|
cause: Throwable?
|
||||||
|
) : IOException(message ?: "Unable to contact node registration service", cause)
|
||||||
|
|
||||||
class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) :
|
class NodeRegistrationHelper(
|
||||||
NetworkRegistrationHelper(
|
private val config: NodeConfiguration,
|
||||||
|
certService: NetworkRegistrationService,
|
||||||
|
regConfig: NodeRegistrationOption,
|
||||||
|
computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))
|
||||||
|
) : NetworkRegistrationHelper(
|
||||||
config.certificatesDirectory,
|
config.certificatesDirectory,
|
||||||
config.signingCertificateStore,
|
config.signingCertificateStore,
|
||||||
config.myLegalName,
|
config.myLegalName,
|
||||||
|
@ -222,7 +222,7 @@ class DriverDSLImpl(
|
|||||||
|
|
||||||
val registrationFuture = if (compatibilityZone?.rootCert != null) {
|
val registrationFuture = if (compatibilityZone?.rootCert != null) {
|
||||||
// We don't need the network map to be available to be able to register the node
|
// We don't need the network map to be available to be able to register the node
|
||||||
startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.doormanURL())
|
startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.config())
|
||||||
} else {
|
} else {
|
||||||
doneFuture(Unit)
|
doneFuture(Unit)
|
||||||
}
|
}
|
||||||
@ -275,14 +275,18 @@ class DriverDSLImpl(
|
|||||||
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap, additionalCordapps, regenerateCordappsOnStart)
|
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap, additionalCordapps, regenerateCordappsOnStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NodeConfig> {
|
private fun startNodeRegistration(
|
||||||
|
providedName: CordaX500Name,
|
||||||
|
rootCert: X509Certificate,
|
||||||
|
networkServicesConfig: NetworkServicesConfig
|
||||||
|
): CordaFuture<NodeConfig> {
|
||||||
val baseDirectory = baseDirectory(providedName).createDirectories()
|
val baseDirectory = baseDirectory(providedName).createDirectories()
|
||||||
val config = NodeConfig(ConfigHelper.loadConfig(
|
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||||
baseDirectory = baseDirectory,
|
baseDirectory = baseDirectory,
|
||||||
allowMissingConfig = true,
|
allowMissingConfig = true,
|
||||||
configOverrides = configOf(
|
configOverrides = configOf(
|
||||||
"p2pAddress" to portAllocation.nextHostAndPort().toString(),
|
"p2pAddress" to portAllocation.nextHostAndPort().toString(),
|
||||||
"compatibilityZoneURL" to compatibilityZoneURL.toString(),
|
"compatibilityZoneURL" to networkServicesConfig.doormanURL.toString(),
|
||||||
"myLegalName" to providedName.toString(),
|
"myLegalName" to providedName.toString(),
|
||||||
"rpcSettings" to mapOf(
|
"rpcSettings" to mapOf(
|
||||||
"address" to portAllocation.nextHostAndPort().toString(),
|
"address" to portAllocation.nextHostAndPort().toString(),
|
||||||
@ -305,7 +309,7 @@ class DriverDSLImpl(
|
|||||||
executorService.fork {
|
executorService.fork {
|
||||||
NodeRegistrationHelper(
|
NodeRegistrationHelper(
|
||||||
config.corda,
|
config.corda,
|
||||||
HTTPNetworkRegistrationService(compatibilityZoneURL, versionInfo),
|
HTTPNetworkRegistrationService(networkServicesConfig, versionInfo),
|
||||||
NodeRegistrationOption(rootTruststorePath, rootTruststorePassword)
|
NodeRegistrationOption(rootTruststorePath, rootTruststorePassword)
|
||||||
).buildKeystore()
|
).buildKeystore()
|
||||||
config
|
config
|
||||||
@ -371,7 +375,7 @@ class DriverDSLImpl(
|
|||||||
startNotaryIdentityGeneration()
|
startNotaryIdentityGeneration()
|
||||||
} else {
|
} else {
|
||||||
// With a root cert specified we delegate generation of the notary identities to the CZ.
|
// With a root cert specified we delegate generation of the notary identities to the CZ.
|
||||||
startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone.doormanURL())
|
startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone)
|
||||||
}
|
}
|
||||||
notaryInfosFuture.map { notaryInfos ->
|
notaryInfosFuture.map { notaryInfos ->
|
||||||
compatibilityZone.publishNotaries(notaryInfos)
|
compatibilityZone.publishNotaries(notaryInfos)
|
||||||
@ -422,16 +426,22 @@ class DriverDSLImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startAllNotaryRegistrations(rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<List<NotaryInfo>> {
|
private fun startAllNotaryRegistrations(
|
||||||
|
rootCert: X509Certificate,
|
||||||
|
compatibilityZone: CompatibilityZoneParams): CordaFuture<List<NotaryInfo>> {
|
||||||
// Start the registration process for all the notaries together then wait for their responses.
|
// Start the registration process for all the notaries together then wait for their responses.
|
||||||
return notarySpecs.map { spec ->
|
return notarySpecs.map { spec ->
|
||||||
require(spec.cluster == null) { "Registering distributed notaries not supported" }
|
require(spec.cluster == null) { "Registering distributed notaries not supported" }
|
||||||
startNotaryRegistration(spec, rootCert, compatibilityZoneURL)
|
startNotaryRegistration(spec, rootCert, compatibilityZone)
|
||||||
}.transpose()
|
}.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNotaryRegistration(spec: NotarySpec, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture<NotaryInfo> {
|
private fun startNotaryRegistration(
|
||||||
return startNodeRegistration(spec.name, rootCert, compatibilityZoneURL).flatMap { config ->
|
spec: NotarySpec,
|
||||||
|
rootCert: X509Certificate,
|
||||||
|
compatibilityZone: CompatibilityZoneParams
|
||||||
|
): CordaFuture<NotaryInfo> {
|
||||||
|
return startNodeRegistration(spec.name, rootCert, compatibilityZone.config()).flatMap { config ->
|
||||||
// Node registration only gives us the node CA cert, not the identity cert. That is only created on first
|
// Node registration only gives us the node CA cert, not the identity cert. That is only created on first
|
||||||
// startup or when the node is told to just generate its node info file. We do that here.
|
// startup or when the node is told to just generate its node info file. We do that here.
|
||||||
if (startNodesInProcess) {
|
if (startNodesInProcess) {
|
||||||
@ -1067,6 +1077,7 @@ sealed class CompatibilityZoneParams(
|
|||||||
) {
|
) {
|
||||||
abstract fun networkMapURL(): URL
|
abstract fun networkMapURL(): URL
|
||||||
abstract fun doormanURL(): URL
|
abstract fun doormanURL(): URL
|
||||||
|
abstract fun config() : NetworkServicesConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1074,11 +1085,18 @@ sealed class CompatibilityZoneParams(
|
|||||||
*/
|
*/
|
||||||
class SharedCompatibilityZoneParams(
|
class SharedCompatibilityZoneParams(
|
||||||
private val url: URL,
|
private val url: URL,
|
||||||
|
private val pnm : UUID?,
|
||||||
publishNotaries: (List<NotaryInfo>) -> Unit,
|
publishNotaries: (List<NotaryInfo>) -> Unit,
|
||||||
rootCert: X509Certificate? = null
|
rootCert: X509Certificate? = null
|
||||||
) : CompatibilityZoneParams(publishNotaries, rootCert) {
|
) : CompatibilityZoneParams(publishNotaries, rootCert) {
|
||||||
|
|
||||||
|
val config : NetworkServicesConfig by lazy {
|
||||||
|
NetworkServicesConfig(url, url, pnm, false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun doormanURL() = url
|
override fun doormanURL() = url
|
||||||
override fun networkMapURL() = url
|
override fun networkMapURL() = url
|
||||||
|
override fun config() : NetworkServicesConfig = config
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1087,11 +1105,17 @@ class SharedCompatibilityZoneParams(
|
|||||||
class SplitCompatibilityZoneParams(
|
class SplitCompatibilityZoneParams(
|
||||||
private val doormanURL: URL,
|
private val doormanURL: URL,
|
||||||
private val networkMapURL: URL,
|
private val networkMapURL: URL,
|
||||||
|
private val pnm : UUID?,
|
||||||
publishNotaries: (List<NotaryInfo>) -> Unit,
|
publishNotaries: (List<NotaryInfo>) -> Unit,
|
||||||
rootCert: X509Certificate? = null
|
rootCert: X509Certificate? = null
|
||||||
) : CompatibilityZoneParams(publishNotaries, rootCert) {
|
) : CompatibilityZoneParams(publishNotaries, rootCert) {
|
||||||
|
val config : NetworkServicesConfig by lazy {
|
||||||
|
NetworkServicesConfig(doormanURL, networkMapURL, pnm, false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun doormanURL() = doormanURL
|
override fun doormanURL() = doormanURL
|
||||||
override fun networkMapURL() = networkMapURL
|
override fun networkMapURL() = networkMapURL
|
||||||
|
override fun config() : NetworkServicesConfig = config
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <A> internalDriver(
|
fun <A> internalDriver(
|
||||||
|
Loading…
Reference in New Issue
Block a user