Non-ssl artemis acceptor for RPC connection. (#271)

* New non-ssl acceptor in artemis server for RPC connection.
This commit is contained in:
Patrick Kuo 2017-02-24 15:36:36 +00:00 committed by GitHub
parent ce2da918c8
commit f0d82e4918
25 changed files with 170 additions and 180 deletions

View File

@ -12,10 +12,10 @@ import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.CordaRPCClient import net.corda.node.services.messaging.CordaRPCClient
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.testing.configureTestSSL
import net.corda.testing.node.NodeBasedTest import net.corda.testing.node.NodeBasedTest
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
@ -37,7 +37,7 @@ class CordaRPCClientTest : NodeBasedTest() {
@Before @Before
fun setUp() { fun setUp() {
node = startNode("Alice", rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow() node = startNode("Alice", rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
client = CordaRPCClient(node.configuration.artemisAddress, configureTestSSL()) client = CordaRPCClient(node.configuration.rpcAddress!!)
} }
@After @After

View File

@ -25,8 +25,6 @@ import net.corda.flows.CashPaymentFlow
import net.corda.node.driver.DriverBasedTest import net.corda.node.driver.DriverBasedTest
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
@ -57,9 +55,10 @@ class NodeMonitorModelTest : DriverBasedTest() {
) )
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser)) val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
aliceNode = aliceNodeFuture.getOrThrow().nodeInfo val notaryNodeHandle = notaryNodeFuture.getOrThrow()
notaryNode = notaryNodeFuture.getOrThrow().nodeInfo aliceNode = aliceNodeHandle.nodeInfo
notaryNode = notaryNodeHandle.nodeInfo
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo } newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
val monitor = NodeMonitorModel() val monitor = NodeMonitorModel()
@ -70,7 +69,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed() vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed() networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password) monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
rpc = monitor.proxyObservable.value!! rpc = monitor.proxyObservable.value!!
runTest() runTest()
} }

View File

@ -52,8 +52,8 @@ class NodeMonitorModel {
* Register for updates to/from a given vault. * Register for updates to/from a given vault.
* TODO provide an unsubscribe mechanism * TODO provide an unsubscribe mechanism
*/ */
fun register(nodeHostAndPort: HostAndPort, sslConfig: SSLConfiguration, username: String, password: String) { fun register(nodeHostAndPort: HostAndPort, username: String, password: String) {
val client = CordaRPCClient(nodeHostAndPort, sslConfig){ val client = CordaRPCClient(nodeHostAndPort){
maxRetryInterval = 10.seconds.toMillis() maxRetryInterval = 10.seconds.toMillis()
} }
client.start(username, password) client.start(username, password)

View File

@ -6,6 +6,7 @@ import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC
import net.corda.testing.messaging.SimpleMQClient import net.corda.testing.messaging.SimpleMQClient
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test import org.junit.Test
@ -14,6 +15,9 @@ import org.junit.Test
* Runs the security tests with the attacker pretending to be a node on the network. * Runs the security tests with the attacker pretending to be a node on the network.
*/ */
class MQSecurityAsNodeTest : MQSecurityTest() { class MQSecurityAsNodeTest : MQSecurityTest() {
override fun createAttacker(): SimpleMQClient {
return clientTo(alice.configuration.artemisAddress)
}
override fun startAttacker(attacker: SimpleMQClient) { override fun startAttacker(attacker: SimpleMQClient) {
attacker.start(PEER_USER, PEER_USER) // Login as a peer attacker.start(PEER_USER, PEER_USER) // Login as a peer
@ -47,4 +51,20 @@ class MQSecurityAsNodeTest : MQSecurityTest() {
attacker.start() attacker.start()
} }
} }
@Test
fun `login to a non ssl port as a node user`() {
val attacker = clientTo(alice.configuration.rpcAddress!!, sslConfiguration = null)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start(NODE_USER, NODE_USER)
}
}
@Test
fun `login to a non ssl port as a peer user`() {
val attacker = clientTo(alice.configuration.rpcAddress!!, sslConfiguration = null)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.start(PEER_USER, PEER_USER) // Login as a peer
}
}
} }

View File

@ -2,14 +2,35 @@ package net.corda.services.messaging
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.testing.messaging.SimpleMQClient import net.corda.testing.messaging.SimpleMQClient
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test
/** /**
* Runs the security tests with the attacker being a valid RPC user of Alice. * Runs the security tests with the attacker being a valid RPC user of Alice.
*/ */
class MQSecurityAsRPCTest : MQSecurityTest() { class MQSecurityAsRPCTest : MQSecurityTest() {
override fun createAttacker(): SimpleMQClient {
return clientTo(alice.configuration.rpcAddress!!)
}
@Test
fun `send message on logged in user's RPC address`() {
val user1Queue = loginToRPCAndGetClientQueue()
assertSendAttackFails(user1Queue)
}
override val extraRPCUsers = listOf(User("evil", "pass", permissions = emptySet())) override val extraRPCUsers = listOf(User("evil", "pass", permissions = emptySet()))
override fun startAttacker(attacker: SimpleMQClient) { override fun startAttacker(attacker: SimpleMQClient) {
attacker.loginToRPC(extraRPCUsers[0]) attacker.loginToRPC(extraRPCUsers[0])
} }
@Test
fun `login to a ssl port as a RPC user`() {
val attacker = clientTo(alice.configuration.artemisAddress)
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
attacker.loginToRPC(extraRPCUsers[0], enableSSL = true)
}
}
} }

View File

@ -14,7 +14,6 @@ import net.corda.core.utilities.unwrap
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.config.SSLConfiguration import net.corda.node.services.config.SSLConfiguration
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_QUEUE import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_QUEUE
@ -24,6 +23,7 @@ import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEE
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_QUEUE_REMOVALS_QUEUE import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_QUEUE_REMOVALS_QUEUE
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
import net.corda.node.services.messaging.CordaRPCClientImpl import net.corda.node.services.messaging.CordaRPCClientImpl
import net.corda.testing.configureTestSSL
import net.corda.testing.messaging.SimpleMQClient import net.corda.testing.messaging.SimpleMQClient
import net.corda.testing.node.NodeBasedTest import net.corda.testing.node.NodeBasedTest
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
@ -49,12 +49,14 @@ abstract class MQSecurityTest : NodeBasedTest() {
@Before @Before
fun start() { fun start() {
alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser).getOrThrow() alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser).getOrThrow()
attacker = clientTo(alice.configuration.artemisAddress) attacker = createAttacker()
startAttacker(attacker) startAttacker(attacker)
} }
open val extraRPCUsers: List<User> get() = emptyList() open val extraRPCUsers: List<User> get() = emptyList()
abstract fun createAttacker(): SimpleMQClient
abstract fun startAttacker(attacker: SimpleMQClient) abstract fun startAttacker(attacker: SimpleMQClient)
@After @After
@ -112,12 +114,6 @@ abstract class MQSecurityTest : NodeBasedTest() {
assertConsumeAttackFails(user1Queue) assertConsumeAttackFails(user1Queue)
} }
@Test
fun `send message on logged in user's RPC address`() {
val user1Queue = loginToRPCAndGetClientQueue()
assertSendAttackFails(user1Queue)
}
@Test @Test
fun `create queue for valid RPC user`() { fun `create queue for valid RPC user`() {
val user1Queue = "$CLIENTS_PREFIX${rpcUser.username}.rpc.${random63BitValue()}" val user1Queue = "$CLIENTS_PREFIX${rpcUser.username}.rpc.${random63BitValue()}"
@ -152,26 +148,26 @@ abstract class MQSecurityTest : NodeBasedTest() {
assertAllQueueCreationAttacksFail(randomQueue) assertAllQueueCreationAttacksFail(randomQueue)
} }
fun clientTo(target: HostAndPort, config: SSLConfiguration = configureTestSSL()): SimpleMQClient { fun clientTo(target: HostAndPort, sslConfiguration: SSLConfiguration? = configureTestSSL()): SimpleMQClient {
val client = SimpleMQClient(target, config) val client = SimpleMQClient(target, sslConfiguration)
clients += client clients += client
return client return client
} }
fun loginToRPC(target: HostAndPort, rpcUser: User): SimpleMQClient { fun loginToRPC(target: HostAndPort, rpcUser: User): SimpleMQClient {
val client = clientTo(target) val client = clientTo(target, null)
client.loginToRPC(rpcUser) client.loginToRPC(rpcUser)
return client return client
} }
fun SimpleMQClient.loginToRPC(rpcUser: User): CordaRPCOps { fun SimpleMQClient.loginToRPC(rpcUser: User, enableSSL: Boolean = false): CordaRPCOps {
start(rpcUser.username, rpcUser.password) start(rpcUser.username, rpcUser.password, enableSSL)
val clientImpl = CordaRPCClientImpl(session, ReentrantLock(), rpcUser.username) val clientImpl = CordaRPCClientImpl(session, ReentrantLock(), rpcUser.username)
return clientImpl.proxyFor(CordaRPCOps::class.java, timeout = 1.seconds) return clientImpl.proxyFor(CordaRPCOps::class.java, timeout = 1.seconds)
} }
fun loginToRPCAndGetClientQueue(): String { fun loginToRPCAndGetClientQueue(): String {
val rpcClient = loginToRPC(alice.configuration.artemisAddress, rpcUser) val rpcClient = loginToRPC(alice.configuration.rpcAddress!!, rpcUser)
val clientQueueQuery = SimpleString("$CLIENTS_PREFIX${rpcUser.username}.rpc.*") val clientQueueQuery = SimpleString("$CLIENTS_PREFIX${rpcUser.username}.rpc.*")
return rpcClient.session.addressQuery(clientQueueQuery).queueNames.single().toString() return rpcClient.session.addressQuery(clientQueueQuery).queueNames.single().toString()
} }

View File

@ -105,7 +105,7 @@ data class NodeHandle(
val configuration: FullNodeConfiguration, val configuration: FullNodeConfiguration,
val process: Process val process: Process
) { ) {
fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.artemisAddress, configuration) fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!)
} }
sealed class PortAllocation { sealed class PortAllocation {
@ -343,7 +343,8 @@ open class DriverDSL(
override fun startNode(providedName: String?, advertisedServices: Set<ServiceInfo>, override fun startNode(providedName: String?, advertisedServices: Set<ServiceInfo>,
rpcUsers: List<User>, customOverrides: Map<String, Any?>): ListenableFuture<NodeHandle> { rpcUsers: List<User>, customOverrides: Map<String, Any?>): ListenableFuture<NodeHandle> {
val messagingAddress = portAllocation.nextHostAndPort() val messagingAddress = portAllocation.nextHostAndPort()
val apiAddress = portAllocation.nextHostAndPort() val rpcAddress = portAllocation.nextHostAndPort()
val webAddress = portAllocation.nextHostAndPort()
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val name = providedName ?: "${pickA(name)}-${messagingAddress.port}" val name = providedName ?: "${pickA(name)}-${messagingAddress.port}"
@ -351,7 +352,8 @@ open class DriverDSL(
val configOverrides = mapOf( val configOverrides = mapOf(
"myLegalName" to name, "myLegalName" to name,
"artemisAddress" to messagingAddress.toString(), "artemisAddress" to messagingAddress.toString(),
"webAddress" to apiAddress.toString(), "rpcAddress" to rpcAddress.toString(),
"webAddress" to webAddress.toString(),
"extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() },
"networkMapService" to mapOf( "networkMapService" to mapOf(
"address" to networkMapAddress.toString(), "address" to networkMapAddress.toString(),

View File

@ -22,11 +22,7 @@ import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.ArtemisMessagingServer
import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.messaging.NodeMessagingClient
import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.services.transactions.*
import net.corda.node.services.transactions.RaftUniquenessProvider
import net.corda.node.services.transactions.RaftValidatingNotaryService
import net.corda.node.services.transactions.BFTSmartUniquenessProvider
import net.corda.node.services.transactions.BFTValidatingNotaryService
import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.AffinityExecutor
import net.corda.node.utilities.databaseTransaction import net.corda.node.utilities.databaseTransaction
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@ -130,7 +126,7 @@ class Node(override val configuration: FullNodeConfiguration,
val serverAddress = with(configuration) { val serverAddress = with(configuration) {
messagingServerAddress ?: { messagingServerAddress ?: {
messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService) messageBroker = ArtemisMessagingServer(this, artemisAddress, rpcAddress, services.networkMapCache, userService)
artemisAddress artemisAddress
}() }()
} }

View File

@ -15,7 +15,6 @@ import net.corda.core.div
import net.corda.core.exists import net.corda.core.exists
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import java.net.URL import java.net.URL
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.time.Instant import java.time.Instant
@ -49,6 +48,9 @@ object ConfigHelper {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
operator fun <T> Config.getValue(receiver: Any, metadata: KProperty<*>): T { operator fun <T> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
if (metadata.returnType.isMarkedNullable && !hasPath(metadata.name)) {
return null as T
}
return when (metadata.returnType.javaType) { return when (metadata.returnType.javaType) {
String::class.java -> getString(metadata.name) as T String::class.java -> getString(metadata.name) as T
Int::class.java -> getInt(metadata.name) as T Int::class.java -> getInt(metadata.name) as T
@ -100,7 +102,7 @@ inline fun <reified T : Any> Config.getListOrElse(path: String, default: Config.
*/ */
fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName) fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
private fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) { fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) {
certificatesDirectory.createDirectories() certificatesDirectory.createDirectories()
if (!trustStoreFile.exists()) { if (!trustStoreFile.exists()) {
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile) javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile)
@ -112,15 +114,3 @@ private fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String)
X509Utilities.createKeystoreForSSL(keyStoreFile, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName) X509Utilities.createKeystoreForSSL(keyStoreFile, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
} }
} }
// TODO Move this to CoreTestUtils.kt once we can pry this from the explorer
@JvmOverloads
fun configureTestSSL(legalName: String = "Mega Corp."): SSLConfiguration = object : SSLConfiguration {
override val certificatesDirectory = Files.createTempDirectory("certs")
override val keyStorePassword: String get() = "cordacadevpass"
override val trustStorePassword: String get() = "trustpass"
init {
configureDevKeyAndTrustStores(legalName)
}
}

View File

@ -65,13 +65,14 @@ class FullNodeConfiguration(override val baseDirectory: Path, val config: Config
} }
val useHTTPS: Boolean by config val useHTTPS: Boolean by config
val artemisAddress: HostAndPort by config val artemisAddress: HostAndPort by config
val rpcAddress: HostAndPort? by config
val webAddress: HostAndPort by config val webAddress: HostAndPort by config
// TODO This field is slightly redundant as artemisAddress is sufficient to hold the address of the node's MQ broker. // TODO This field is slightly redundant as artemisAddress is sufficient to hold the address of the node's MQ broker.
// Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one
val messagingServerAddress: HostAndPort? by config.getOrElse { null } val messagingServerAddress: HostAndPort? by config
val extraAdvertisedServiceIds: List<String> = config.getListOrElse<String>("extraAdvertisedServiceIds") { emptyList() } val extraAdvertisedServiceIds: List<String> = config.getListOrElse<String>("extraAdvertisedServiceIds") { emptyList() }
val useTestClock: Boolean by config.getOrElse { false } val useTestClock: Boolean by config.getOrElse { false }
val notaryNodeAddress: HostAndPort? by config.getOrElse { null } val notaryNodeAddress: HostAndPort? by config
val notaryClusterAddresses: List<HostAndPort> = config val notaryClusterAddresses: List<HostAndPort> = config
.getListOrElse<String>("notaryClusterAddresses") { emptyList() } .getListOrElse<String>("notaryClusterAddresses") { emptyList() }
.map { HostAndPort.fromString(it) } .map { HostAndPort.fromString(it) }

View File

@ -21,7 +21,7 @@ import java.security.KeyStore
/** /**
* The base class for Artemis services that defines shared data structures and SSL transport configuration. * The base class for Artemis services that defines shared data structures and SSL transport configuration.
*/ */
abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() { abstract class ArtemisMessagingComponent : SingletonSerializeAsToken() {
companion object { companion object {
init { init {
System.setProperty("org.jboss.logging.provider", "slf4j") System.setProperty("org.jboss.logging.provider", "slf4j")
@ -85,6 +85,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress { fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort) return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort)
} }
fun asService(serviceIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress { fun asService(serviceIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort) return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort)
} }
@ -134,7 +135,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
} }
} }
protected fun tcpTransport(direction: ConnectionDirection, host: String, port: Int): TransportConfiguration { protected fun tcpTransport(direction: ConnectionDirection, host: String, port: Int, enableSSL: Boolean = true): TransportConfiguration {
val config = config val config = config
val options = mutableMapOf<String, Any?>( val options = mutableMapOf<String, Any?>(
// Basic TCP target details // Basic TCP target details
@ -148,7 +149,7 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP" TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP"
) )
if (config != null) { if (config != null && enableSSL) {
config.keyStoreFile.expectedOnDefaultFileSystem() config.keyStoreFile.expectedOnDefaultFileSystem()
config.trustStoreFile.expectedOnDefaultFileSystem() config.trustStoreFile.expectedOnDefaultFileSystem()
val tlsOptions = mapOf<String, Any?>( val tlsOptions = mapOf<String, Any?>(

View File

@ -81,7 +81,8 @@ import javax.security.cert.X509Certificate
*/ */
@ThreadSafe @ThreadSafe
class ArtemisMessagingServer(override val config: NodeConfiguration, class ArtemisMessagingServer(override val config: NodeConfiguration,
val myHostPort: HostAndPort, val artemisHostPort: HostAndPort,
val rpcHostPort: HostAndPort?,
val networkMapCache: NetworkMapCache, val networkMapCache: NetworkMapCache,
val userService: RPCUserService) : ArtemisMessagingComponent() { val userService: RPCUserService) : ArtemisMessagingComponent() {
companion object { companion object {
@ -139,7 +140,10 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
} }
activeMQServer.start() activeMQServer.start()
printBasicNodeInfo("Node listening on address", myHostPort.toString()) printBasicNodeInfo("Node listening on address", artemisHostPort.toString())
if (rpcHostPort != null) {
printBasicNodeInfo("Node RPC service listening on address", rpcHostPort.toString())
}
} }
private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply { private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply {
@ -147,7 +151,11 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
bindingsDirectory = (artemisDir / "bindings").toString() bindingsDirectory = (artemisDir / "bindings").toString()
journalDirectory = (artemisDir / "journal").toString() journalDirectory = (artemisDir / "journal").toString()
largeMessagesDirectory = (artemisDir / "large-messages").toString() largeMessagesDirectory = (artemisDir / "large-messages").toString()
acceptorConfigurations = setOf(tcpTransport(Inbound, "0.0.0.0", myHostPort.port)) val acceptors = mutableSetOf(tcpTransport(Inbound, "0.0.0.0", artemisHostPort.port))
if (rpcHostPort != null) {
acceptors.add(tcpTransport(Inbound, "0.0.0.0", rpcHostPort.port, enableSSL = false))
}
acceptorConfigurations = acceptors
// Enable built in message deduplication. Note we still have to do our own as the delayed commits // Enable built in message deduplication. Note we still have to do our own as the delayed commits
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates. // and our own definition of commit mean that the built in deduplication cannot remove all duplicates.
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
@ -397,8 +405,7 @@ private class VerifyingNettyConnector(configuration: MutableMap<String, Any>?,
threadPool: Executor?, threadPool: Executor?,
scheduledThreadPool: ScheduledExecutorService?, scheduledThreadPool: ScheduledExecutorService?,
protocolManager: ClientProtocolManager?) : protocolManager: ClientProtocolManager?) :
NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager) {
{
private val server = configuration?.get(ArtemisMessagingServer::class.java.name) as? ArtemisMessagingServer private val server = configuration?.get(ArtemisMessagingServer::class.java.name) as? ArtemisMessagingServer
private val expectedCommonName = configuration?.get(ArtemisMessagingComponent.VERIFY_PEER_COMMON_NAME) as? String private val expectedCommonName = configuration?.get(ArtemisMessagingComponent.VERIFY_PEER_COMMON_NAME) as? String
@ -480,15 +487,15 @@ class NodeLoginModule : LoginModule {
val username = nameCallback.name ?: throw FailedLoginException("Username not provided") val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided")) val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
val certificates = certificateCallback.certificates
log.info("Processing login for $username") log.info("Processing login for $username")
val validatedUser = if (username == PEER_USER || username == NODE_USER) { val validatedUser = when (determineUserRole(certificates, username)) {
val certificates = certificateCallback.certificates ?: throw FailedLoginException("No TLS?") PEER_ROLE -> authenticatePeer(certificates)
authenticateNode(certificates, username) NODE_ROLE -> authenticateNode(certificates)
} else { RPC_ROLE -> authenticateRpcUser(password, username)
// Otherwise assume they're an RPC user else -> throw FailedLoginException("Peer does not belong on our network")
authenticateRpcUser(password, username)
} }
principals += UserPrincipal(validatedUser) principals += UserPrincipal(validatedUser)
@ -496,22 +503,22 @@ class NodeLoginModule : LoginModule {
return loginSucceeded return loginSucceeded
} }
private fun authenticateNode(certificates: Array<X509Certificate>, username: String): String { private fun authenticateNode(certificates: Array<X509Certificate>): String {
val peerCertificate = certificates.first() val peerCertificate = certificates.first()
val role = if (username == NODE_USER) {
if (peerCertificate.publicKey != ourPublicKey) { if (peerCertificate.publicKey != ourPublicKey) {
throw FailedLoginException("Only the node can login as $NODE_USER") throw FailedLoginException("Only the node can login as $NODE_USER")
} }
NODE_ROLE principals += RolePrincipal(NODE_ROLE)
} else { return peerCertificate.subjectDN.name
}
private fun authenticatePeer(certificates: Array<X509Certificate>): String {
val theirRootCAPublicKey = certificates.last().publicKey val theirRootCAPublicKey = certificates.last().publicKey
if (theirRootCAPublicKey != ourRootCAPublicKey) { if (theirRootCAPublicKey != ourRootCAPublicKey) {
throw FailedLoginException("Peer does not belong on our network. Their root CA: $theirRootCAPublicKey") throw FailedLoginException("Peer does not belong on our network. Their root CA: $theirRootCAPublicKey")
} }
PEER_ROLE // This enables the peer to send to our P2P address principals += RolePrincipal(PEER_ROLE)
} return certificates.first().subjectDN.name
principals += RolePrincipal(role)
return peerCertificate.subjectDN.name
} }
private fun authenticateRpcUser(password: String, username: String): String { private fun authenticateRpcUser(password: String, username: String): String {
@ -526,6 +533,18 @@ class NodeLoginModule : LoginModule {
return username return username
} }
private fun determineUserRole(certificates: Array<X509Certificate>?, username: String): String? {
return if (username == PEER_USER || username == NODE_USER) {
certificates ?: throw FailedLoginException("No TLS?")
if (username == PEER_USER) PEER_ROLE else NODE_ROLE
} else if (certificates == null) {
// Assume they're an RPC user if its from a non-ssl connection
RPC_ROLE
} else {
null
}
}
override fun commit(): Boolean { override fun commit(): Boolean {
val result = loginSucceeded val result = loginSucceeded
if (result) { if (result) {

View File

@ -24,10 +24,10 @@ import javax.annotation.concurrent.ThreadSafe
* useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works. * useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works.
* *
* @param host The hostname and messaging port of the node. * @param host The hostname and messaging port of the node.
* @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will not be authenticated, nor will RPC traffic be encrypted. * @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will only be authenticated on non-SSL RPC port, the RPC traffic with not be encrypted when SSL is disabled.
*/ */
@ThreadSafe @ThreadSafe
class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration?, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() { class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration? = null, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() {
private companion object { private companion object {
val log = loggerFor<CordaRPCClient>() val log = loggerFor<CordaRPCClient>()
} }

View File

@ -47,6 +47,7 @@ class ArtemisMessagingTests {
@Rule @JvmField val temporaryFolder = TemporaryFolder() @Rule @JvmField val temporaryFolder = TemporaryFolder()
val hostAndPort = freeLocalHostAndPort() val hostAndPort = freeLocalHostAndPort()
val rpcHostAndPort = freeLocalHostAndPort()
val topic = "platform.self" val topic = "platform.self"
val identity = generateKeyPair() val identity = generateKeyPair()
@ -230,8 +231,8 @@ class ArtemisMessagingTests {
} }
} }
private fun createMessagingServer(local: HostAndPort = hostAndPort): ArtemisMessagingServer { private fun createMessagingServer(local: HostAndPort = hostAndPort, rpc: HostAndPort = rpcHostAndPort): ArtemisMessagingServer {
return ArtemisMessagingServer(config, local, networkMapCache, userService).apply { return ArtemisMessagingServer(config, local, rpc, networkMapCache, userService).apply {
config.configureWithDevSSLCertificate() config.configureWithDevSSLCertificate()
messagingServer = this messagingServer = this
} }

View File

@ -9,7 +9,6 @@ import net.corda.core.messaging.startFlow
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.CordaRPCClient import net.corda.node.services.messaging.CordaRPCClient
import net.corda.testing.http.HttpApi import net.corda.testing.http.HttpApi
@ -26,11 +25,12 @@ class BankOfCordaClientApi(val hostAndPort: HostAndPort) {
val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot)
return api.postJson("issue-asset-request", params) return api.postJson("issue-asset-request", params)
} }
/** /**
* RPC API * RPC API
*/ */
fun requestRPCIssue(params: IssueRequestParams): SignedTransaction { fun requestRPCIssue(params: IssueRequestParams): SignedTransaction {
val client = CordaRPCClient(hostAndPort, configureTestSSL()) val client = CordaRPCClient(hostAndPort)
// TODO: privileged security controls required // TODO: privileged security controls required
client.start("bankUser", "test") client.start("bankUser", "test")
val proxy = client.proxy() val proxy = client.proxy()

View File

@ -55,7 +55,7 @@ class IRSDemoTest : IntegrationTestCategory {
} }
fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable<LocalDate?> { fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable<LocalDate?> {
val client = CordaRPCClient(config.artemisAddress, config) val client = CordaRPCClient(config.rpcAddress!!)
client.start("user", "password") client.start("user", "password")
val proxy = client.proxy() val proxy = client.proxy()
val vaultUpdates = proxy.vaultAndUpdates().second val vaultUpdates = proxy.vaultAndUpdates().second

View File

@ -27,7 +27,7 @@ class TraderDemoTest : NodeBasedTest() {
).getOrThrow() ).getOrThrow()
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
val client = CordaRPCClient(it.configuration.artemisAddress, it.configuration) val client = CordaRPCClient(it.configuration.rpcAddress!!)
client.start(demoUser[0].username, demoUser[0].password).proxy() client.start(demoUser[0].username, demoUser[0].password).proxy()
} }

View File

@ -18,6 +18,8 @@ import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.node.internal.AbstractNode import net.corda.node.internal.AbstractNode
import net.corda.node.internal.NetworkMapInfo import net.corda.node.internal.NetworkMapInfo
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.SSLConfiguration
import net.corda.node.services.config.configureDevKeyAndTrustStores
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.ADD
import net.corda.testing.node.MockIdentityService import net.corda.testing.node.MockIdentityService
@ -25,6 +27,7 @@ import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDataSourceProperties
import java.net.ServerSocket import java.net.ServerSocket
import java.net.URL import java.net.URL
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.util.* import java.util.*
@ -161,3 +164,14 @@ data class TestNodeConfiguration(
override val certificateSigningService: URL = URL("http://localhost")) : NodeConfiguration override val certificateSigningService: URL = URL("http://localhost")) : NodeConfiguration
fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name)) fun Config.getHostAndPort(name: String) = HostAndPort.fromString(getString(name))
@JvmOverloads
fun configureTestSSL(legalName: String = "Mega Corp."): SSLConfiguration = object : SSLConfiguration {
override val certificatesDirectory = Files.createTempDirectory("certs")
override val keyStorePassword: String get() = "cordacadevpass"
override val trustStorePassword: String get() = "trustpass"
init {
configureDevKeyAndTrustStores(legalName)
}
}

View File

@ -2,22 +2,22 @@ package net.corda.testing.messaging
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import net.corda.node.services.config.SSLConfiguration import net.corda.node.services.config.SSLConfiguration
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
import net.corda.testing.configureTestSSL
import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.*
/** /**
* As the name suggests this is a simple client for connecting to MQ brokers. * As the name suggests this is a simple client for connecting to MQ brokers.
*/ */
class SimpleMQClient(val target: HostAndPort, class SimpleMQClient(val target: HostAndPort,
override val config: SSLConfiguration = configureTestSSL("SimpleMQClient")) : ArtemisMessagingComponent() { override val config: SSLConfiguration? = configureTestSSL("SimpleMQClient")) : ArtemisMessagingComponent() {
lateinit var sessionFactory: ClientSessionFactory lateinit var sessionFactory: ClientSessionFactory
lateinit var session: ClientSession lateinit var session: ClientSession
lateinit var producer: ClientProducer lateinit var producer: ClientProducer
fun start(username: String? = null, password: String? = null) { fun start(username: String? = null, password: String? = null, enableSSL: Boolean = true) {
val tcpTransport = tcpTransport(Outbound(), target.hostText, target.port) val tcpTransport = tcpTransport(Outbound(), target.hostText, target.port, enableSSL)
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
isBlockOnNonDurableSend = true isBlockOnNonDurableSend = true
threadPoolMaxSize = 1 threadPoolMaxSize = 1

View File

@ -121,6 +121,7 @@ abstract class NodeBasedTest {
configOverrides = mapOf( configOverrides = mapOf(
"myLegalName" to legalName, "myLegalName" to legalName,
"artemisAddress" to freeLocalHostAndPort().toString(), "artemisAddress" to freeLocalHostAndPort().toString(),
"rpcAddress" to freeLocalHostAndPort().toString(),
"extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() },
"rpcUsers" to rpcUsers.map { "rpcUsers" to rpcUsers.map {
mapOf( mapOf(

View File

@ -23,14 +23,14 @@ import kotlin.concurrent.thread
* This is a bare-bones node which can only send and receive messages. It doesn't register with a network map service or * This is a bare-bones node which can only send and receive messages. It doesn't register with a network map service or
* any other such task that would make it functionable in a network and thus left to the user to do so manually. * any other such task that would make it functionable in a network and thus left to the user to do so manually.
*/ */
class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort()) : AutoCloseable { class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), rpcAddress: HostAndPort = freeLocalHostAndPort()) : AutoCloseable {
private val databaseWithCloseable: Pair<Closeable, Database> = configureDatabase(config.dataSourceProperties) private val databaseWithCloseable: Pair<Closeable, Database> = configureDatabase(config.dataSourceProperties)
val database: Database get() = databaseWithCloseable.second val database: Database get() = databaseWithCloseable.second
val userService = RPCUserServiceImpl(config) val userService = RPCUserServiceImpl(config)
val identity: KeyPair = generateKeyPair() val identity: KeyPair = generateKeyPair()
val executor = ServiceAffinityExecutor(config.myLegalName, 1) val executor = ServiceAffinityExecutor(config.myLegalName, 1)
val broker = ArtemisMessagingServer(config, address, InMemoryNetworkMapCache(), userService) val broker = ArtemisMessagingServer(config, address, rpcAddress, InMemoryNetworkMapCache(), userService)
val networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create<Unit>() val networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create<Unit>()
val net = databaseTransaction(database) { val net = databaseTransaction(database) {
NodeMessagingClient( NodeMessagingClient(

View File

@ -14,7 +14,6 @@ import net.corda.client.model.Models
import net.corda.client.model.observableValue import net.corda.client.model.observableValue
import net.corda.core.contracts.GBP import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD import net.corda.core.contracts.USD
import net.corda.core.messaging.startFlow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.explorer.model.CordaViewModel import net.corda.explorer.model.CordaViewModel
@ -28,7 +27,6 @@ import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.node.driver.PortAllocation import net.corda.node.driver.PortAllocation
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import org.apache.commons.lang.SystemUtils import org.apache.commons.lang.SystemUtils
@ -141,7 +139,7 @@ fun main(args: Array<String>) {
val issuerNodeUSD = issuerUSD.get() val issuerNodeUSD = issuerUSD.get()
arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach { arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}") println("${it.nodeInfo.legalIdentity} started on ${it.configuration.rpcAddress}")
} }
val parser = OptionParser("S") val parser = OptionParser("S")

View File

@ -30,9 +30,6 @@ class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable {
private var username: String by config private var username: String by config
private var reportingCurrency: Currency by config private var reportingCurrency: Currency by config
private var fullscreen: Boolean by config private var fullscreen: Boolean by config
private var certificatesDir: Path by config
private var keyStorePassword: String by config
private var trustStorePassword: String by config
// Create observable Properties. // Create observable Properties.
val reportingCurrencyProperty = writableConfigProperty(SettingsModel::reportingCurrency) val reportingCurrencyProperty = writableConfigProperty(SettingsModel::reportingCurrency)
@ -41,10 +38,6 @@ class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable {
val portProperty = writableConfigProperty(SettingsModel::port) val portProperty = writableConfigProperty(SettingsModel::port)
val usernameProperty = writableConfigProperty(SettingsModel::username) val usernameProperty = writableConfigProperty(SettingsModel::username)
val fullscreenProperty = writableConfigProperty(SettingsModel::fullscreen) val fullscreenProperty = writableConfigProperty(SettingsModel::fullscreen)
val certificatesDirProperty = writableConfigProperty(SettingsModel::certificatesDir)
// TODO : We should encrypt all passwords in config file.
val keyStorePasswordProperty = writableConfigProperty(SettingsModel::keyStorePassword)
val trustStorePasswordProperty = writableConfigProperty(SettingsModel::trustStorePassword)
init { init {
load() load()

View File

@ -5,16 +5,16 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.property.SimpleIntegerProperty import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.* import javafx.scene.control.*
import javafx.stage.FileChooser
import net.corda.client.fxutils.map import net.corda.client.fxutils.map
import net.corda.client.model.NodeMonitorModel import net.corda.client.model.NodeMonitorModel
import net.corda.client.model.objectProperty import net.corda.client.model.objectProperty
import net.corda.core.exists
import net.corda.explorer.model.SettingsModel import net.corda.explorer.model.SettingsModel
import net.corda.node.services.config.SSLConfiguration import net.corda.node.services.config.SSLConfiguration
import net.corda.node.services.config.configureTestSSL
import org.controlsfx.dialog.ExceptionDialog import org.controlsfx.dialog.ExceptionDialog
import tornadofx.* import tornadofx.*
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths
import kotlin.system.exitProcess import kotlin.system.exitProcess
class LoginView : View() { class LoginView : View() {
@ -26,7 +26,6 @@ class LoginView : View() {
private val passwordTextField by fxid<PasswordField>() private val passwordTextField by fxid<PasswordField>()
private val rememberMeCheckBox by fxid<CheckBox>() private val rememberMeCheckBox by fxid<CheckBox>()
private val fullscreenCheckBox by fxid<CheckBox>() private val fullscreenCheckBox by fxid<CheckBox>()
private val certificateButton by fxid<Button>()
private val portProperty = SimpleIntegerProperty() private val portProperty = SimpleIntegerProperty()
private val rememberMe by objectProperty(SettingsModel::rememberMeProperty) private val rememberMe by objectProperty(SettingsModel::rememberMeProperty)
@ -34,9 +33,6 @@ class LoginView : View() {
private val host by objectProperty(SettingsModel::hostProperty) private val host by objectProperty(SettingsModel::hostProperty)
private val port by objectProperty(SettingsModel::portProperty) private val port by objectProperty(SettingsModel::portProperty)
private val fullscreen by objectProperty(SettingsModel::fullscreenProperty) private val fullscreen by objectProperty(SettingsModel::fullscreenProperty)
private val certificatesDir by objectProperty(SettingsModel::certificatesDirProperty)
private val keyStorePasswordProperty by objectProperty(SettingsModel::keyStorePasswordProperty)
private val trustStorePasswordProperty by objectProperty(SettingsModel::trustStorePasswordProperty)
fun login() { fun login() {
val status = Dialog<LoginStatus>().apply { val status = Dialog<LoginStatus>().apply {
@ -46,7 +42,7 @@ class LoginView : View() {
ButtonBar.ButtonData.OK_DONE -> try { ButtonBar.ButtonData.OK_DONE -> try {
root.isDisable = true root.isDisable = true
// TODO : Run this async to avoid UI lockup. // TODO : Run this async to avoid UI lockup.
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), configureSSL(), usernameTextField.text, passwordTextField.text) getModel<NodeMonitorModel>().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), usernameTextField.text, passwordTextField.text)
if (!rememberMe.value) { if (!rememberMe.value) {
username.value = "" username.value = ""
host.value = "" host.value = ""
@ -79,18 +75,6 @@ class LoginView : View() {
if (status != LoginStatus.loggedIn) login() if (status != LoginStatus.loggedIn) login()
} }
private fun configureSSL(): SSLConfiguration {
val sslConfig = object : SSLConfiguration {
override val certificatesDirectory: Path get() = certificatesDir.get()
override val keyStorePassword: String get() = keyStorePasswordProperty.get()
override val trustStorePassword: String get() = trustStorePasswordProperty.get()
}
// TODO : Don't use dev certificates.
return if (sslConfig.keyStoreFile.exists()) sslConfig else configureTestSSL().apply {
alert(Alert.AlertType.WARNING, "", "KeyStore not found in certificates directory.\nDEV certificates will be used by default.")
}
}
init { init {
// Restrict text field to Integer only. // Restrict text field to Integer only.
portTextField.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) } portTextField.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) }
@ -99,44 +83,6 @@ class LoginView : View() {
usernameTextField.textProperty().bindBidirectional(username) usernameTextField.textProperty().bindBidirectional(username)
hostTextField.textProperty().bindBidirectional(host) hostTextField.textProperty().bindBidirectional(host)
portTextField.textProperty().bindBidirectional(port) portTextField.textProperty().bindBidirectional(port)
certificateButton.setOnAction {
Dialog<ButtonType>().apply {
title = "Certificates Settings"
initOwner(root.scene.window)
dialogPane.content = gridpane {
vgap = 10.0
hgap = 5.0
row("Certificates Directory :") {
textfield {
prefWidth = 400.0
textProperty().bind(certificatesDir.map(Path::toString))
isEditable = false
}
button {
graphic = FontAwesomeIconView(FontAwesomeIcon.FOLDER_OPEN_ALT)
maxHeight = Double.MAX_VALUE
setOnAction {
chooseDirectory(owner = dialogPane.scene.window) {
initialDirectoryProperty().bind(certificatesDir.map(Path::toFile))
}?.let {
certificatesDir.set(it.toPath())
}
}
}
}
row("KeyStore Password :") { passwordfield(keyStorePasswordProperty) }
row("TrustStore Password :") { passwordfield(trustStorePasswordProperty) }
}
dialogPane.buttonTypes.addAll(ButtonType.APPLY, ButtonType.CANCEL)
}.showAndWait().get().let {
when (it) {
ButtonType.APPLY -> getModel<SettingsModel>().commit()
// Discard changes.
else -> getModel<SettingsModel>().load()
}
}
}
certificateButton.tooltip("Certificate Configuration")
} }
private enum class LoginStatus { private enum class LoginStatus {

View File

@ -26,23 +26,15 @@
<Label text="Corda Node :" GridPane.halignment="RIGHT"/> <Label text="Corda Node :" GridPane.halignment="RIGHT"/>
<TextField fx:id="hostTextField" promptText="Host" GridPane.columnIndex="1"/> <TextField fx:id="hostTextField" promptText="Host" GridPane.columnIndex="1"/>
<TextField fx:id="portTextField" prefWidth="100" promptText="Port" GridPane.columnIndex="2"/> <TextField fx:id="portTextField" prefWidth="100" promptText="Port" GridPane.columnIndex="2"/>
<Button id="certificateButton" fx:id="certificateButton" GridPane.columnIndex="3" styleClass="certificateButton">
<padding>
<Insets right="6"/>
</padding>
<graphic>
<FontAwesomeIconView styleClass="certificateIcon" glyphName="LOCK" glyphSize="20"/>
</graphic>
</Button>
<Label text="Username :" GridPane.rowIndex="1" GridPane.halignment="RIGHT"/> <Label text="Username :" GridPane.rowIndex="1" GridPane.halignment="RIGHT"/>
<TextField fx:id="usernameTextField" promptText="Username" GridPane.columnIndex="1" <TextField fx:id="usernameTextField" promptText="Username" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="1"/> GridPane.columnSpan="2" GridPane.rowIndex="1"/>
<Label text="Password :" GridPane.rowIndex="2" GridPane.halignment="RIGHT"/> <Label text="Password :" GridPane.rowIndex="2" GridPane.halignment="RIGHT"/>
<PasswordField fx:id="passwordTextField" promptText="Password" GridPane.columnIndex="1" <PasswordField fx:id="passwordTextField" promptText="Password" GridPane.columnIndex="1"
GridPane.columnSpan="3" GridPane.rowIndex="2"/> GridPane.columnSpan="2" GridPane.rowIndex="2"/>
<HBox spacing="20" GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="3"> <HBox spacing="20" GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="2">
<CheckBox fx:id="rememberMeCheckBox" text="Remember me"/> <CheckBox fx:id="rememberMeCheckBox" text="Remember me"/>
<CheckBox fx:id="fullscreenCheckBox" text="Fullscreen mode"/> <CheckBox fx:id="fullscreenCheckBox" text="Fullscreen mode"/>
</HBox> </HBox>