ENT-11384: Cleanup JarScanningCordappLoader (#7664)

* It uses URLs when in fact CorDapps are jar files, and so should being Path. It also does URL equality, which is not recommended
* Address (very old) TODO of removing RestrictedURL, which is not needed

Also, back-ported some minor changes from https://github.com/corda/enterprise/pull/5057.
This commit is contained in:
Shams Asari 2024-01-29 13:44:14 +00:00 committed by GitHub
parent d642ebfbd7
commit a7d0684fe7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 142 additions and 172 deletions

View File

@ -53,7 +53,6 @@ import kotlin.test.assertFalse
import kotlin.test.assertNull import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
class CordaRPCClientReconnectionTest { class CordaRPCClientReconnectionTest {
private val portAllocator = incrementalPortAllocation() private val portAllocator = incrementalPortAllocation()

View File

@ -109,9 +109,9 @@ dependencies {
smokeTestImplementation project(":client:rpc") smokeTestImplementation project(":client:rpc")
smokeTestImplementation project(':smoke-test-utils') smokeTestImplementation project(':smoke-test-utils')
smokeTestImplementation project(':core-test-utils') smokeTestImplementation project(':core-test-utils')
smokeTestImplementation project(":finance:contracts")
smokeTestImplementation project(":finance:workflows") smokeTestImplementation project(":finance:workflows")
smokeTestImplementation project(":testing:cordapps:4.11-workflows") smokeTestImplementation project(":testing:cordapps:4.11-workflows")
smokeTestImplementation project(":finance:contracts")
smokeTestImplementation "org.assertj:assertj-core:${assertj_version}" smokeTestImplementation "org.assertj:assertj-core:${assertj_version}"
smokeTestImplementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}" smokeTestImplementation "org.bouncycastle:bcprov-jdk18on:${bouncycastle_version}"
smokeTestImplementation "co.paralleluniverse:quasar-core:$quasar_version" smokeTestImplementation "co.paralleluniverse:quasar-core:$quasar_version"

View File

@ -6,7 +6,6 @@ import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.internal.cordapp.JarScanningCordappLoader
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import java.net.URL
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
@ -31,7 +30,7 @@ class AttachmentFixupsTest {
fun `test fixup rule that adds attachment`() { fun `test fixup rule that adds attachment`() {
val fixupJar = Files.createTempFile("fixup", ".jar") val fixupJar = Files.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 => $ID2, $ID3") .writeFixupRules("$ID1 => $ID2, $ID3")
val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { val fixedIDs = with(newFixupService(fixupJar)) {
fixupAttachmentIds(listOf(ID1)) fixupAttachmentIds(listOf(ID1))
} }
assertThat(fixedIDs).containsExactly(ID2, ID3) assertThat(fixedIDs).containsExactly(ID2, ID3)
@ -41,7 +40,7 @@ class AttachmentFixupsTest {
fun `test fixup rule that deletes attachment`() { fun `test fixup rule that deletes attachment`() {
val fixupJar = Files.createTempFile("fixup", ".jar") val fixupJar = Files.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 =>") .writeFixupRules("$ID1 =>")
val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { val fixedIDs = with(newFixupService(fixupJar)) {
fixupAttachmentIds(listOf(ID1)) fixupAttachmentIds(listOf(ID1))
} }
assertThat(fixedIDs).isEmpty() assertThat(fixedIDs).isEmpty()
@ -52,7 +51,7 @@ class AttachmentFixupsTest {
val fixupJar = Files.createTempFile("fixup", ".jar") val fixupJar = Files.createTempFile("fixup", ".jar")
.writeFixupRules(" => $ID2") .writeFixupRules(" => $ID2")
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newFixupService(fixupJar.toUri().toURL()) newFixupService(fixupJar)
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Forbidden empty list of source attachment IDs in '$fixupJar'" "Forbidden empty list of source attachment IDs in '$fixupJar'"
@ -65,7 +64,7 @@ class AttachmentFixupsTest {
val fixupJar = Files.createTempFile("fixup", ".jar") val fixupJar = Files.createTempFile("fixup", ".jar")
.writeFixupRules(rule) .writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newFixupService(fixupJar.toUri().toURL()) newFixupService(fixupJar)
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '$fixupJar'" "Invalid fix-up line '${rule.trim()}' in '$fixupJar'"
@ -78,7 +77,7 @@ class AttachmentFixupsTest {
val fixupJar = Files.createTempFile("fixup", ".jar") val fixupJar = Files.createTempFile("fixup", ".jar")
.writeFixupRules(rule) .writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newFixupService(fixupJar.toUri().toURL()) newFixupService(fixupJar)
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '$fixupJar'" "Invalid fix-up line '${rule.trim()}' in '$fixupJar'"
@ -94,7 +93,7 @@ class AttachmentFixupsTest {
"", "",
"$ID3 => $ID4" "$ID3 => $ID4"
) )
val fixedIDs = with(newFixupService(fixupJar.toUri().toURL())) { val fixedIDs = with(newFixupService(fixupJar)) {
fixupAttachmentIds(listOf(ID2, ID1)) fixupAttachmentIds(listOf(ID2, ID1))
} }
assertThat(fixedIDs).containsExactlyInAnyOrder(ID2, ID4) assertThat(fixedIDs).containsExactlyInAnyOrder(ID2, ID4)
@ -130,8 +129,8 @@ class AttachmentFixupsTest {
} }
} }
private fun newFixupService(vararg urls: URL): AttachmentFixups { private fun newFixupService(vararg paths: Path): AttachmentFixups {
val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(paths.toSet(), VersionInfo.UNKNOWN)
return AttachmentFixups().apply { load(loader.appClassLoader) } return AttachmentFixups().apply { load(loader.appClassLoader) }
} }
} }

View File

@ -138,27 +138,19 @@ fun <T> List<T>.indexOfOrThrow(item: T): Int {
* Similar to [Iterable.map] except it maps to a [Set] which preserves the iteration order. * Similar to [Iterable.map] except it maps to a [Set] which preserves the iteration order.
*/ */
@Suppress("INVISIBLE_MEMBER", "RemoveExplicitTypeArguments") // Because the external verifier uses Kotlin 1.2 @Suppress("INVISIBLE_MEMBER", "RemoveExplicitTypeArguments") // Because the external verifier uses Kotlin 1.2
inline fun <T, R> Iterable<T>.mapToSet(transform: (T) -> R): Set<R> { inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> {
return if (this is Collection) { return when (size) {
when (size) {
0 -> return emptySet() 0 -> return emptySet()
1 -> return setOf(transform(first())) 1 -> return setOf(transform(first()))
else -> mapTo(LinkedHashSet<R>(mapCapacity(size)), transform) else -> mapTo(LinkedHashSet<R>(mapCapacity(size)), transform)
} }
} else {
mapTo(LinkedHashSet<R>(), transform)
}
} }
/** /**
* Similar to [Iterable.flatMap] except it maps to a [Set] which preserves the iteration order. * Similar to [Iterable.flatMap] except it maps to a [Set] which preserves the iteration order.
*/ */
inline fun <T, R> Iterable<T>.flatMapToSet(transform: (T) -> Iterable<R>): Set<R> { inline fun <T, R> Collection<T>.flatMapToSet(transform: (T) -> Iterable<R>): Set<R> {
return if (this is Collection && isEmpty()) { return if (isEmpty()) emptySet() else flatMapTo(LinkedHashSet(), transform)
emptySet()
} else {
flatMapTo(LinkedHashSet(), transform)
}
} }
fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options) fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)

View File

@ -6,7 +6,6 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.internal.PLATFORM_VERSION import net.corda.core.internal.PLATFORM_VERSION
import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.toPath
import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryComponent
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.CheckpointCustomSerializer import net.corda.core.serialization.CheckpointCustomSerializer
@ -14,10 +13,12 @@ import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsToken
import java.net.URL import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import kotlin.io.path.name import kotlin.io.path.name
data class CordappImpl( data class CordappImpl(
val jarFile: Path,
override val contractClassNames: List<String>, override val contractClassNames: List<String>,
override val initiatedFlows: List<Class<out FlowLogic<*>>>, override val initiatedFlows: List<Class<out FlowLogic<*>>>,
override val rpcFlows: List<Class<out FlowLogic<*>>>, override val rpcFlows: List<Class<out FlowLogic<*>>>,
@ -30,7 +31,6 @@ data class CordappImpl(
override val checkpointCustomSerializers: List<CheckpointCustomSerializer<*, *>>, override val checkpointCustomSerializers: List<CheckpointCustomSerializer<*, *>>,
override val customSchemas: Set<MappedSchema>, override val customSchemas: Set<MappedSchema>,
override val allFlows: List<Class<out FlowLogic<*>>>, override val allFlows: List<Class<out FlowLogic<*>>>,
override val jarPath: URL,
override val info: Cordapp.Info, override val info: Cordapp.Info,
override val jarHash: SecureHash.SHA256, override val jarHash: SecureHash.SHA256,
override val minimumPlatformVersion: Int, override val minimumPlatformVersion: Int,
@ -41,7 +41,11 @@ data class CordappImpl(
private val explicitCordappClasses: List<String> = emptyList(), private val explicitCordappClasses: List<String> = emptyList(),
val isVirtual: Boolean = false val isVirtual: Boolean = false
) : Cordapp { ) : Cordapp {
override val name: String = jarName(jarPath) override val jarPath: URL
get() = jarFile.toUri().toURL()
override val name: String
get() = jarName(jarFile)
// TODO: Also add [SchedulableFlow] as a Cordapp class // TODO: Also add [SchedulableFlow] as a Cordapp class
override val cordappClasses: List<String> = run { override val cordappClasses: List<String> = run {
@ -50,7 +54,7 @@ data class CordappImpl(
} }
companion object { companion object {
fun jarName(url: URL): String = url.toPath().name.removeSuffix(".jar") fun jarName(url: Path): String = url.name.removeSuffix(".jar")
/** CorDapp manifest entries */ /** CorDapp manifest entries */
const val CORDAPP_CONTRACT_NAME = "Cordapp-Contract-Name" const val CORDAPP_CONTRACT_NAME = "Cordapp-Contract-Name"
@ -74,6 +78,7 @@ data class CordappImpl(
@VisibleForTesting @VisibleForTesting
val TEST_INSTANCE = CordappImpl( val TEST_INSTANCE = CordappImpl(
jarFile = Paths.get(""),
contractClassNames = emptyList(), contractClassNames = emptyList(),
initiatedFlows = emptyList(), initiatedFlows = emptyList(),
rpcFlows = emptyList(), rpcFlows = emptyList(),
@ -85,7 +90,6 @@ data class CordappImpl(
serializationCustomSerializers = emptyList(), serializationCustomSerializers = emptyList(),
checkpointCustomSerializers = emptyList(), checkpointCustomSerializers = emptyList(),
customSchemas = emptySet(), customSchemas = emptySet(),
jarPath = Paths.get("").toUri().toURL(),
info = UNKNOWN_INFO, info = UNKNOWN_INFO,
allFlows = emptyList(), allFlows = emptyList(),
jarHash = SecureHash.allOnesHash, jarHash = SecureHash.allOnesHash,

View File

@ -4,6 +4,7 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TransactionState import net.corda.core.contracts.TransactionState
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.internal.Version import net.corda.core.internal.Version
import net.corda.core.internal.mapToSet
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
/** /**
@ -14,6 +15,8 @@ import net.corda.core.serialization.CordaSerializable
@CordaSerializable @CordaSerializable
class MissingContractAttachments class MissingContractAttachments
@JvmOverloads @JvmOverloads
constructor(val states: List<TransactionState<ContractState>>, contractsClassName: String? = null, minimumRequiredContractClassVersion: Version? = null) : FlowException( constructor(val states: List<TransactionState<ContractState>>,
"Cannot find contract attachments for " + contractsClassName: String? = null,
"${contractsClassName ?: states.map { it.contract }.distinct()}${minimumRequiredContractClassVersion?.let { ", minimum required contract class version $minimumRequiredContractClassVersion"}}.") minimumRequiredContractClassVersion: Version? = null
) : FlowException("Cannot find contract attachments for " +
"${contractsClassName ?: states.mapToSet { it.contract }}${minimumRequiredContractClassVersion?.let { ", minimum required contract class version $minimumRequiredContractClassVersion"} ?: ""}.")

View File

@ -25,12 +25,13 @@ import net.corda.core.internal.hash
import net.corda.core.internal.isAbstractClass import net.corda.core.internal.isAbstractClass
import net.corda.core.internal.loadClassOfType import net.corda.core.internal.loadClassOfType
import net.corda.core.internal.location import net.corda.core.internal.location
import net.corda.core.internal.mapToSet
import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.NotaryService
import net.corda.core.internal.notary.SinglePartyNotaryService import net.corda.core.internal.notary.SinglePartyNotaryService
import net.corda.core.internal.objectOrNewInstance import net.corda.core.internal.objectOrNewInstance
import net.corda.core.internal.pooledScan import net.corda.core.internal.pooledScan
import net.corda.core.internal.readFully
import net.corda.core.internal.telemetry.TelemetryComponent import net.corda.core.internal.telemetry.TelemetryComponent
import net.corda.core.internal.toPath
import net.corda.core.internal.toTypedArray import net.corda.core.internal.toTypedArray
import net.corda.core.internal.warnContractWithoutConstraintPropagation import net.corda.core.internal.warnContractWithoutConstraintPropagation
import net.corda.core.node.services.CordaService import net.corda.core.node.services.CordaService
@ -46,7 +47,6 @@ import net.corda.nodeapi.internal.coreContractClasses
import net.corda.serialization.internal.DefaultWhitelist import net.corda.serialization.internal.DefaultWhitelist
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.math.BigInteger import java.math.BigInteger
import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.nio.file.Path import java.nio.file.Path
import java.util.Random import java.util.Random
@ -55,30 +55,36 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
import java.util.jar.Manifest import java.util.jar.Manifest
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.useDirectoryEntries import kotlin.io.path.inputStream
import kotlin.io.path.isSameFileAs
import kotlin.io.path.listDirectoryEntries
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
* Handles CorDapp loading and classpath scanning of CorDapp JARs * Handles CorDapp loading and classpath scanning of CorDapp JARs
* *
* @property cordappJarPaths The classpath of cordapp JARs * @property cordappJars The classpath of cordapp JARs
*/ */
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>, class JarScanningCordappLoader private constructor(private val cordappJars: Set<Path>,
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl>, extraCordapps: List<CordappImpl>,
private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoaderTemplate() { private val signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()) : CordappLoaderTemplate() {
init { init {
if (cordappJarPaths.isEmpty()) { if (cordappJars.isEmpty()) {
logger.info("No CorDapp paths provided") logger.info("No CorDapp paths provided")
} else { } else {
logger.info("Loading CorDapps from ${cordappJarPaths.joinToString()}") logger.info("Loading CorDapps from ${cordappJars.joinToString()}")
} }
} }
private val cordappClasses: ConcurrentHashMap<String, Set<Cordapp>> = ConcurrentHashMap() private val cordappClasses: ConcurrentHashMap<String, Set<Cordapp>> = ConcurrentHashMap()
override val cordapps: List<CordappImpl> by lazy { loadCordapps() + extraCordapps } override val cordapps: List<CordappImpl> by lazy { loadCordapps() + extraCordapps }
override val appClassLoader: URLClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader) override val appClassLoader: URLClassLoader = URLClassLoader(
cordappJars.stream().map { it.toUri().toURL() }.toTypedArray(),
javaClass.classLoader
)
override fun close() = appClassLoader.close() override fun close() = appClassLoader.close()
@ -95,7 +101,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
extraCordapps: List<CordappImpl> = emptyList(), extraCordapps: List<CordappImpl> = emptyList(),
signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader { signerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } val paths = cordappDirs
.asSequence()
.flatMap { if (it.exists()) it.listDirectoryEntries("*.jar") else emptyList() }
.toSet()
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist) return JarScanningCordappLoader(paths, versionInfo, extraCordapps, signerKeyFingerprintBlacklist)
} }
@ -104,50 +113,40 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
* *
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
*/ */
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(), fun fromJarUrls(scanJars: Set<Path>,
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
extraCordapps: List<CordappImpl> = emptyList(),
cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader { cordappsSignerKeyFingerprintBlacklist: List<SecureHash> = emptyList()): JarScanningCordappLoader {
val paths = scanJars.map { it.restricted() } return JarScanningCordappLoader(scanJars, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist)
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, cordappsSignerKeyFingerprintBlacklist)
}
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
private fun jarUrlsInDirectory(directory: Path): List<URL> {
return if (!directory.exists()) {
emptyList()
} else {
directory.useDirectoryEntries("*.jar") { jars -> jars.map { it.toUri().toURL() }.toList() }
}
} }
} }
private fun loadCordapps(): List<CordappImpl> { private fun loadCordapps(): List<CordappImpl> {
val invalidCordapps = mutableMapOf<String, URL>() val invalidCordapps = mutableMapOf<String, Path>()
val cordapps = cordappJarPaths val cordapps = cordappJars
.map { url -> scanCordapp(url).use { it.toCordapp(url) } } .map { path -> scanCordapp(path).use { it.toCordapp(path) } }
.filter { .filter { cordapp ->
if (it.minimumPlatformVersion > versionInfo.platformVersion) { if (cordapp.minimumPlatformVersion > versionInfo.platformVersion) {
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum " + logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it requires minimum " +
"platform version ${it.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).") "platform version ${cordapp.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
invalidCordapps.put("CorDapp requires minimumPlatformVersion: ${it.minimumPlatformVersion}, but was: ${versionInfo.platformVersion}", it.jarPath) invalidCordapps["CorDapp requires minimumPlatformVersion: ${cordapp.minimumPlatformVersion}, but was: ${versionInfo.platformVersion}"] = cordapp.jarFile
false false
} else { } else {
true true
} }
} }.filter { cordapp ->
.filter {
if (signerKeyFingerprintBlacklist.isEmpty()) { if (signerKeyFingerprintBlacklist.isEmpty()) {
true //Nothing blacklisted, no need to check true //Nothing blacklisted, no need to check
} else { } else {
val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) val certificates = cordapp.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates)
val blockedCertificates = certificates.filter { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist } val blockedCertificates = certificates.filter { it.publicKey.hash.sha256() in signerKeyFingerprintBlacklist }
if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty()) if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty()) {
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
else { } else {
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by blacklisted key(s) only (probably development key): " + logger.warn("Not loading CorDapp ${cordapp.info.shortName} (${cordapp.info.vendor}) as it is signed by blacklisted key(s) only (probably development key): " +
"${blockedCertificates.map { it.publicKey }}.") "${blockedCertificates.map { it.publicKey }}.")
invalidCordapps.put("Corresponding contracts are signed by blacklisted key(s) only (probably development key),", it.jarPath) invalidCordapps["Corresponding contracts are signed by blacklisted key(s) only (probably development key),"] = cordapp.jarFile
false false
} }
} }
@ -185,14 +184,15 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
} }
} }
private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl { private fun RestrictedScanResult.toCordapp(path: Path): CordappImpl {
val manifest: Manifest? = url.url.openStream().use { JarInputStream(it).manifest } val manifest: Manifest? = JarInputStream(path.inputStream()).use { it.manifest }
val info = parseCordappInfo(manifest, CordappImpl.jarName(url.url)) val info = parseCordappInfo(manifest, CordappImpl.jarName(path))
val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1 val minPlatformVersion = manifest?.get(CordappImpl.MIN_PLATFORM_VERSION)?.toIntOrNull() ?: 1
val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion val targetPlatformVersion = manifest?.get(CordappImpl.TARGET_PLATFORM_VERSION)?.toIntOrNull() ?: minPlatformVersion
validateContractStateClassVersion(this) validateContractStateClassVersion(this)
validateWhitelistClassVersion(this) validateWhitelistClassVersion(this)
return CordappImpl( return CordappImpl(
path,
findContractClassNamesWithVersionCheck(this), findContractClassNamesWithVersionCheck(this),
findInitiatedFlows(this), findInitiatedFlows(this),
findRPCFlows(this), findRPCFlows(this),
@ -200,14 +200,13 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
findSchedulableFlows(this), findSchedulableFlows(this),
findServices(this), findServices(this),
findTelemetryComponents(this), findTelemetryComponents(this),
findWhitelists(url), findWhitelists(path),
findSerializers(this), findSerializers(this),
findCheckpointSerializers(this), findCheckpointSerializers(this),
findCustomSchemas(this), findCustomSchemas(this),
findAllFlows(this), findAllFlows(this),
url.url,
info, info,
getJarHash(url.url), path.hash,
minPlatformVersion, minPlatformVersion,
targetPlatformVersion, targetPlatformVersion,
findNotaryService(this), findNotaryService(this),
@ -291,8 +290,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
return result.firstOrNull() return result.firstOrNull()
} }
private fun getJarHash(url: URL): SecureHash.SHA256 = url.openStream().readFully().sha256()
private fun findServices(scanResult: RestrictedScanResult): List<Class<out SerializeAsToken>> { private fun findServices(scanResult: RestrictedScanResult): List<Class<out SerializeAsToken>> {
return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class)
} }
@ -345,10 +342,10 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
scanResult.versionCheckClassesImplementing(SerializationWhitelist::class) scanResult.versionCheckClassesImplementing(SerializationWhitelist::class)
} }
private fun findWhitelists(cordappJarPath: RestrictedURL): List<SerializationWhitelist> { private fun findWhitelists(cordappJar: Path): List<SerializationWhitelist> {
val whitelists = ServiceLoader.load(SerializationWhitelist::class.java, appClassLoader).toList() val whitelists = ServiceLoader.load(SerializationWhitelist::class.java, appClassLoader).toList()
return whitelists.filter { return whitelists.filter {
it.javaClass.location == cordappJarPath.url && it.javaClass.name.startsWith(cordappJarPath.qualifiedNamePrefix) it.javaClass.location.toPath().isSameFileAs(cordappJar)
} + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app. } + DefaultWhitelist // Always add the DefaultWhitelist to the whitelist for an app.
} }
@ -361,19 +358,18 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
} }
private fun findCustomSchemas(scanResult: RestrictedScanResult): Set<MappedSchema> { private fun findCustomSchemas(scanResult: RestrictedScanResult): Set<MappedSchema> {
return scanResult.getClassesWithSuperclass(MappedSchema::class).instances().toSet() return scanResult.getClassesWithSuperclass(MappedSchema::class).mapToSet { it.kotlin.objectOrNewInstance() }
} }
private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult { private fun scanCordapp(cordappJar: Path): RestrictedScanResult {
val cordappElement = cordappJarPath.url.toString() logger.info("Scanning CorDapp ${cordappJar.absolutePathString()}")
logger.info("Scanning CorDapp in $cordappElement")
val scanResult = ClassGraph() val scanResult = ClassGraph()
.filterClasspathElementsByURL { elt -> elt == cordappJarPath.url } .filterClasspathElementsByURL { it.toPath().isSameFileAs(cordappJar) }
.overrideClassLoaders(appClassLoader) .overrideClassLoaders(appClassLoader)
.ignoreParentClassLoaders() .ignoreParentClassLoaders()
.enableAllInfo() .enableAllInfo()
.pooledScan() .pooledScan()
return RestrictedScanResult(scanResult, cordappJarPath.qualifiedNamePrefix, cordappJarPath) return RestrictedScanResult(scanResult, cordappJar)
} }
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? { private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
@ -388,28 +384,16 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
} }
} }
// TODO Remove this class as rootPackageName is never non-null. private inner class RestrictedScanResult(private val scanResult: ScanResult, private val cordappJar: Path) : AutoCloseable {
/** @property rootPackageName only this package and subpackages may be extracted from [url], or null to allow all packages. */
private data class RestrictedURL(val url: URL, val rootPackageName: String?) {
val qualifiedNamePrefix: String get() = rootPackageName?.let { "$it." } ?: ""
}
private fun <T : Any> List<Class<out T>>.instances(): List<T> {
return map { it.kotlin.objectOrNewInstance() }
}
private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String,
private val cordappJarPath: RestrictedURL) : AutoCloseable {
fun getNamesOfClassesImplementingWithClassVersionCheck(type: KClass<*>): List<String> { fun getNamesOfClassesImplementingWithClassVersionCheck(type: KClass<*>): List<String> {
return scanResult.getClassesImplementing(type.java.name).filter { it.name.startsWith(qualifiedNamePrefix) }.map { return scanResult.getClassesImplementing(type.java.name).map {
validateClassFileVersion(it) validateClassFileVersion(it)
it.name it.name
} }
} }
fun versionCheckClassesImplementing(type: KClass<*>) { fun versionCheckClassesImplementing(type: KClass<*>) {
return scanResult.getClassesImplementing(type.java.name).filter { it.name.startsWith(qualifiedNamePrefix) }.forEach { return scanResult.getClassesImplementing(type.java.name).forEach {
validateClassFileVersion(it) validateClassFileVersion(it)
} }
} }
@ -418,7 +402,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
return scanResult return scanResult
.getSubclasses(type.java.name) .getSubclasses(type.java.name)
.names .names
.filter { it.startsWith(qualifiedNamePrefix) }
.mapNotNull { loadClass(it, type) } .mapNotNull { loadClass(it, type) }
.filterNot { it.isAbstractClass } .filterNot { it.isAbstractClass }
} }
@ -426,7 +409,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
fun <T : Any> getClassesImplementingWithClassVersionCheck(type: KClass<T>): List<T> { fun <T : Any> getClassesImplementingWithClassVersionCheck(type: KClass<T>): List<T> {
return scanResult return scanResult
.getClassesImplementing(type.java.name) .getClassesImplementing(type.java.name)
.filter { it.name.startsWith(qualifiedNamePrefix) }
.mapNotNull { .mapNotNull {
validateClassFileVersion(it) validateClassFileVersion(it)
loadClass(it.name, type) } loadClass(it.name, type) }
@ -437,9 +419,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
fun <T : Any> getClassesImplementing(type: KClass<T>): List<Class<out T>> { fun <T : Any> getClassesImplementing(type: KClass<T>): List<Class<out T>> {
return scanResult return scanResult
.getClassesImplementing(type.java.name) .getClassesImplementing(type.java.name)
.filter { it.name.startsWith(qualifiedNamePrefix) } .mapNotNull { loadClass(it.name, type) }
.mapNotNull {
loadClass(it.name, type) }
.filterNot { it.isAbstractClass } .filterNot { it.isAbstractClass }
} }
@ -447,7 +427,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
return scanResult return scanResult
.getClassesWithAnnotation(annotation.java.name) .getClassesWithAnnotation(annotation.java.name)
.names .names
.filter { it.startsWith(qualifiedNamePrefix) }
.mapNotNull { loadClass(it, type) } .mapNotNull { loadClass(it, type) }
.filterNot { Modifier.isAbstract(it.modifiers) } .filterNot { Modifier.isAbstract(it.modifiers) }
} }
@ -456,29 +435,18 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
return scanResult return scanResult
.getSubclasses(type.java.name) .getSubclasses(type.java.name)
.names .names
.filter { it.startsWith(qualifiedNamePrefix) }
.mapNotNull { loadClass(it, type) } .mapNotNull { loadClass(it, type) }
.filterNot { it.isAbstractClass } .filterNot { it.isAbstractClass }
} }
fun getAllStandardClasses(): List<String> { fun getAllStandardClasses(): List<String> = scanResult.allStandardClasses.names
return scanResult
.allStandardClasses
.names
.filter { it.startsWith(qualifiedNamePrefix) }
}
fun getAllInterfaces(): List<String> { fun getAllInterfaces(): List<String> = scanResult.allInterfaces.names
return scanResult
.allInterfaces
.names
.filter { it.startsWith(qualifiedNamePrefix) }
}
private fun validateClassFileVersion(classInfo: ClassInfo) { private fun validateClassFileVersion(classInfo: ClassInfo) {
if (classInfo.classfileMajorVersion < JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION || if (classInfo.classfileMajorVersion < JAVA_1_2_CLASS_FILE_FORMAT_MAJOR_VERSION ||
classInfo.classfileMajorVersion > JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION) classInfo.classfileMajorVersion > JAVA_17_CLASS_FILE_FORMAT_MAJOR_VERSION)
throw IllegalStateException("Class ${classInfo.name} from jar file ${cordappJarPath.url} has an invalid version of " + throw IllegalStateException("Class ${classInfo.name} from jar file $cordappJar has an invalid version of " +
"${classInfo.classfileMajorVersion}") "${classInfo.classfileMajorVersion}")
} }
@ -524,9 +492,7 @@ class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set<Cordapp>
class InvalidCordappException(message: String) : CordaRuntimeException(message) class InvalidCordappException(message: String) : CordaRuntimeException(message)
abstract class CordappLoaderTemplate : CordappLoader { abstract class CordappLoaderTemplate : CordappLoader {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
} }

View File

@ -5,6 +5,7 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.flows.ContractUpgradeFlow
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.location import net.corda.core.internal.location
import net.corda.core.internal.toPath
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.notary.experimental.bftsmart.BFTSmartNotarySchemaV1 import net.corda.notary.experimental.bftsmart.BFTSmartNotarySchemaV1
import net.corda.notary.experimental.bftsmart.BFTSmartNotaryService import net.corda.notary.experimental.bftsmart.BFTSmartNotaryService
@ -24,6 +25,7 @@ internal object VirtualCordapp {
/** A Cordapp representing the core package which is not scanned automatically. */ /** A Cordapp representing the core package which is not scanned automatically. */
fun generateCore(versionInfo: VersionInfo): CordappImpl { fun generateCore(versionInfo: VersionInfo): CordappImpl {
return CordappImpl( return CordappImpl(
jarFile = ContractUpgradeFlow.javaClass.location.toPath(), // Core JAR location
contractClassNames = listOf(), contractClassNames = listOf(),
initiatedFlows = listOf(), initiatedFlows = listOf(),
rpcFlows = coreRpcFlows, rpcFlows = coreRpcFlows,
@ -37,7 +39,6 @@ internal object VirtualCordapp {
customSchemas = setOf(), customSchemas = setOf(),
info = Cordapp.Info.Default("corda-core", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"), info = Cordapp.Info.Default("corda-core", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"),
allFlows = listOf(), allFlows = listOf(),
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
jarHash = SecureHash.allOnesHash, jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = versionInfo.platformVersion, minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion, targetPlatformVersion = versionInfo.platformVersion,
@ -49,6 +50,7 @@ internal object VirtualCordapp {
/** A Cordapp for the built-in notary service implementation. */ /** A Cordapp for the built-in notary service implementation. */
fun generateJPANotary(versionInfo: VersionInfo): CordappImpl { fun generateJPANotary(versionInfo: VersionInfo): CordappImpl {
return CordappImpl( return CordappImpl(
jarFile = JPANotaryService::class.java.location.toPath(),
contractClassNames = listOf(), contractClassNames = listOf(),
initiatedFlows = listOf(), initiatedFlows = listOf(),
rpcFlows = listOf(), rpcFlows = listOf(),
@ -62,7 +64,6 @@ internal object VirtualCordapp {
customSchemas = setOf(JPANotarySchemaV1), customSchemas = setOf(JPANotarySchemaV1),
info = Cordapp.Info.Default("corda-notary", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"), info = Cordapp.Info.Default("corda-notary", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"),
allFlows = listOf(), allFlows = listOf(),
jarPath = JPANotaryService::class.java.location,
jarHash = SecureHash.allOnesHash, jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = versionInfo.platformVersion, minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion, targetPlatformVersion = versionInfo.platformVersion,
@ -75,6 +76,7 @@ internal object VirtualCordapp {
/** A Cordapp for the built-in Raft notary service implementation. */ /** A Cordapp for the built-in Raft notary service implementation. */
fun generateRaftNotary(versionInfo: VersionInfo): CordappImpl { fun generateRaftNotary(versionInfo: VersionInfo): CordappImpl {
return CordappImpl( return CordappImpl(
jarFile = RaftNotaryService::class.java.location.toPath(),
contractClassNames = listOf(), contractClassNames = listOf(),
initiatedFlows = listOf(), initiatedFlows = listOf(),
rpcFlows = listOf(), rpcFlows = listOf(),
@ -88,7 +90,6 @@ internal object VirtualCordapp {
customSchemas = setOf(RaftNotarySchemaV1), customSchemas = setOf(RaftNotarySchemaV1),
info = Cordapp.Info.Default("corda-notary-raft", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"), info = Cordapp.Info.Default("corda-notary-raft", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"),
allFlows = listOf(), allFlows = listOf(),
jarPath = RaftNotaryService::class.java.location,
jarHash = SecureHash.allOnesHash, jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = versionInfo.platformVersion, minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion, targetPlatformVersion = versionInfo.platformVersion,
@ -100,6 +101,7 @@ internal object VirtualCordapp {
/** A Cordapp for the built-in BFT-Smart notary service implementation. */ /** A Cordapp for the built-in BFT-Smart notary service implementation. */
fun generateBFTSmartNotary(versionInfo: VersionInfo): CordappImpl { fun generateBFTSmartNotary(versionInfo: VersionInfo): CordappImpl {
return CordappImpl( return CordappImpl(
jarFile = BFTSmartNotaryService::class.java.location.toPath(),
contractClassNames = listOf(), contractClassNames = listOf(),
initiatedFlows = listOf(), initiatedFlows = listOf(),
rpcFlows = listOf(), rpcFlows = listOf(),
@ -113,7 +115,6 @@ internal object VirtualCordapp {
customSchemas = setOf(BFTSmartNotarySchemaV1), customSchemas = setOf(BFTSmartNotarySchemaV1),
info = Cordapp.Info.Default("corda-notary-bft-smart", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"), info = Cordapp.Info.Default("corda-notary-bft-smart", versionInfo.vendor, versionInfo.releaseVersion, "Open Source (Apache 2)"),
allFlows = listOf(), allFlows = listOf(),
jarPath = BFTSmartNotaryService::class.java.location,
jarHash = SecureHash.allOnesHash, jarHash = SecureHash.allOnesHash,
minimumPlatformVersion = versionInfo.platformVersion, minimumPlatformVersion = versionInfo.platformVersion,
targetPlatformVersion = versionInfo.platformVersion, targetPlatformVersion = versionInfo.platformVersion,

View File

@ -2,6 +2,7 @@ package net.corda.node.internal.cordapp
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import net.corda.core.internal.toPath
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.AttachmentStorage
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
@ -17,8 +18,8 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.URL
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
import java.util.zip.Deflater.NO_COMPRESSION import java.util.zip.Deflater.NO_COMPRESSION
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -28,10 +29,10 @@ import kotlin.test.assertFailsWith
class CordappProviderImplTests { class CordappProviderImplTests {
private companion object { private companion object {
val isolatedJAR: URL = this::class.java.getResource("/isolated.jar")!! val isolatedJAR = this::class.java.getResource("/isolated.jar")!!.toPath()
// TODO: Cordapp name should differ from the JAR name // TODO: Cordapp name should differ from the JAR name
const val isolatedCordappName = "isolated" const val isolatedCordappName = "isolated"
val emptyJAR: URL = this::class.java.getResource("empty.jar")!! val emptyJAR = this::class.java.getResource("empty.jar")!!.toPath()
val validConfig: Config = ConfigFactory.parseString("key=value") val validConfig: Config = ConfigFactory.parseString("key=value")
@JvmField @JvmField
@ -108,7 +109,7 @@ class CordappProviderImplTests {
fun `test cordapp configuration`() { fun `test cordapp configuration`() {
val configProvider = MockCordappConfigProvider() val configProvider = MockCordappConfigProvider()
configProvider.cordappConfigs[isolatedCordappName] = validConfig configProvider.cordappConfigs[isolatedCordappName] = validConfig
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN)
val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start() } val provider = CordappProviderImpl(loader, configProvider, attachmentStore).apply { start() }
val expected = provider.getAppContext(provider.cordapps.first()).config val expected = provider.getAppContext(provider.cordapps.first()).config
@ -120,7 +121,7 @@ class CordappProviderImplTests {
fun `test fixup rule that adds attachment`() { fun `test fixup rule that adds attachment`() {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 => $ID2, $ID3") .writeFixupRules("$ID1 => $ID2, $ID3")
val fixedIDs = with(newCordappProvider(fixupJar.toURI().toURL())) { val fixedIDs = with(newCordappProvider(fixupJar.toPath())) {
start() start()
attachmentFixups.fixupAttachmentIds(listOf(ID1)) attachmentFixups.fixupAttachmentIds(listOf(ID1))
} }
@ -131,7 +132,7 @@ class CordappProviderImplTests {
fun `test fixup rule that deletes attachment`() { fun `test fixup rule that deletes attachment`() {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules("$ID1 =>") .writeFixupRules("$ID1 =>")
val fixedIDs = with(newCordappProvider(fixupJar.toURI().toURL())) { val fixedIDs = with(newCordappProvider(fixupJar.toPath())) {
start() start()
attachmentFixups.fixupAttachmentIds(listOf(ID1)) attachmentFixups.fixupAttachmentIds(listOf(ID1))
} }
@ -143,7 +144,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(" => $ID2") .writeFixupRules(" => $ID2")
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toURI().toURL()).start() newCordappProvider(fixupJar.toPath()).start()
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Forbidden empty list of source attachment IDs in '${fixupJar.absolutePath}'" "Forbidden empty list of source attachment IDs in '${fixupJar.absolutePath}'"
@ -156,7 +157,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(rule) .writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toURI().toURL()).start() newCordappProvider(fixupJar.toPath()).start()
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'" "Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'"
@ -169,7 +170,7 @@ class CordappProviderImplTests {
val fixupJar = File.createTempFile("fixup", ".jar") val fixupJar = File.createTempFile("fixup", ".jar")
.writeFixupRules(rule) .writeFixupRules(rule)
val ex = assertFailsWith<IllegalArgumentException> { val ex = assertFailsWith<IllegalArgumentException> {
newCordappProvider(fixupJar.toURI().toURL()).start() newCordappProvider(fixupJar.toPath()).start()
} }
assertThat(ex).hasMessageContaining( assertThat(ex).hasMessageContaining(
"Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'" "Invalid fix-up line '${rule.trim()}' in '${fixupJar.absolutePath}'"
@ -185,7 +186,7 @@ class CordappProviderImplTests {
"", "",
"$ID3 => $ID4" "$ID3 => $ID4"
) )
val fixedIDs = with(newCordappProvider(fixupJar.toURI().toURL())) { val fixedIDs = with(newCordappProvider(fixupJar.toPath())) {
start() start()
attachmentFixups.fixupAttachmentIds(listOf(ID2, ID1)) attachmentFixups.fixupAttachmentIds(listOf(ID2, ID1))
} }
@ -200,8 +201,8 @@ class CordappProviderImplTests {
val duplicateJarPath = signedJarPath.parent.resolve("duplicate-${signedJarPath.fileName}") val duplicateJarPath = signedJarPath.parent.resolve("duplicate-${signedJarPath.fileName}")
Files.copy(signedJarPath, duplicateJarPath) Files.copy(signedJarPath, duplicateJarPath)
val urls = listOf(signedJarPath.toUri().toURL(), duplicateJarPath.toUri().toURL()) val paths = setOf(signedJarPath, duplicateJarPath)
JarScanningCordappLoader.fromJarUrls(urls, VersionInfo.UNKNOWN).use { JarScanningCordappLoader.fromJarUrls(paths, VersionInfo.UNKNOWN).use {
assertFailsWith<DuplicateCordappsInstalledException> { assertFailsWith<DuplicateCordappsInstalledException> {
CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() } CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() }
} }
@ -214,8 +215,8 @@ class CordappProviderImplTests {
SelfCleaningDir().use { file -> SelfCleaningDir().use { file ->
val jarA = ContractJarTestUtils.makeTestContractJar(file.path, listOf("com.example.MyContract", "com.example.AnotherContractForA"), generateManifest = false, jarFileName = "sampleA.jar") val jarA = ContractJarTestUtils.makeTestContractJar(file.path, listOf("com.example.MyContract", "com.example.AnotherContractForA"), generateManifest = false, jarFileName = "sampleA.jar")
val jarB = ContractJarTestUtils.makeTestContractJar(file.path, listOf("com.example.MyContract", "com.example.AnotherContractForB"), generateManifest = false, jarFileName = "sampleB.jar") val jarB = ContractJarTestUtils.makeTestContractJar(file.path, listOf("com.example.MyContract", "com.example.AnotherContractForB"), generateManifest = false, jarFileName = "sampleB.jar")
val urls = listOf(jarA.toUri().toURL(), jarB.toUri().toURL()) val paths = setOf(jarA, jarB)
JarScanningCordappLoader.fromJarUrls(urls, VersionInfo.UNKNOWN).use { JarScanningCordappLoader.fromJarUrls(paths, VersionInfo.UNKNOWN).use {
assertFailsWith<IllegalStateException> { assertFailsWith<IllegalStateException> {
CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() } CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() }
} }
@ -238,8 +239,8 @@ class CordappProviderImplTests {
return this return this
} }
private fun newCordappProvider(vararg urls: URL): CordappProviderImpl { private fun newCordappProvider(vararg paths: Path): CordappProviderImpl {
val loader = JarScanningCordappLoader.fromJarUrls(urls.toList(), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(paths.toSet(), VersionInfo.UNKNOWN)
return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start() } return CordappProviderImpl(loader, stubConfigProvider, attachmentStore).apply { start() }
} }
} }

View File

@ -8,6 +8,7 @@ import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.SchedulableFlow import net.corda.core.flows.SchedulableFlow
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.packageName_ import net.corda.core.internal.packageName_
import net.corda.core.internal.toPath
import net.corda.node.VersionInfo import net.corda.node.VersionInfo
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
import net.corda.testing.node.internal.cordappWithPackages import net.corda.testing.node.internal.cordappWithPackages
@ -55,8 +56,8 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `isolated JAR contains a CorDapp with a contract and plugin`() { fun `isolated JAR contains a CorDapp with a contract and plugin`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!! val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)) val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR))
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
@ -68,13 +69,13 @@ class JarScanningCordappLoaderTest {
assertThat(actualCordapp.services).isEmpty() assertThat(actualCordapp.services).isEmpty()
assertThat(actualCordapp.serializationWhitelists).hasSize(1) assertThat(actualCordapp.serializationWhitelists).hasSize(1)
assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.serialization.internal.DefaultWhitelist") assertThat(actualCordapp.serializationWhitelists.first().javaClass.name).isEqualTo("net.corda.serialization.internal.DefaultWhitelist")
assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR) assertThat(actualCordapp.jarFile).isEqualTo(isolatedJAR)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `constructed CordappImpl contains the right cordapp classes`() { fun `constructed CordappImpl contains the right cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!! val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR)) val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR))
val actualCordapp = loader.cordapps.single() val actualCordapp = loader.cordapps.single()
val cordappClasses = actualCordapp.cordappClasses val cordappClasses = actualCordapp.cordappClasses
@ -86,7 +87,7 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `flows are loaded by loader`() { fun `flows are loaded by loader`() {
val jarFile = cordappWithPackages(javaClass.packageName_).jarFile val jarFile = cordappWithPackages(javaClass.packageName_).jarFile
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jarFile.toUri().toURL())) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jarFile))
// One cordapp from this source tree. In gradle it will also pick up the node jar. // One cordapp from this source tree. In gradle it will also pick up the node jar.
assertThat(loader.cordapps).isNotEmpty assertThat(loader.cordapps).isNotEmpty
@ -101,8 +102,8 @@ class JarScanningCordappLoaderTest {
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired. // being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader can load cordapp classes`() { fun `cordapp classloader can load cordapp classes`() {
val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!! val isolatedJAR = JarScanningCordappLoaderTest::class.java.getResource("/isolated.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(isolatedJAR), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(setOf(isolatedJAR), VersionInfo.UNKNOWN)
loader.appClassLoader.loadClass(isolatedContractId) loader.appClassLoader.loadClass(isolatedContractId)
loader.appClassLoader.loadClass(isolatedFlowName) loader.appClassLoader.loadClass(isolatedFlowName)
@ -110,8 +111,8 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader sets target and min version to 1 if not specified`() { fun `cordapp classloader sets target and min version to 1 if not specified`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/no-min-or-target-version.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
loader.cordapps.forEach { loader.cordapps.forEach {
assertThat(it.targetPlatformVersion).isEqualTo(1) assertThat(it.targetPlatformVersion).isEqualTo(1)
assertThat(it.minimumPlatformVersion).isEqualTo(1) assertThat(it.minimumPlatformVersion).isEqualTo(1)
@ -122,8 +123,8 @@ class JarScanningCordappLoaderTest {
fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() { fun `cordapp classloader returns correct values for minPlatformVersion and targetVersion`() {
// load jar with min and target version in manifest // load jar with min and target version in manifest
// make sure classloader extracts correct values // make sure classloader extracts correct values
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
val cordapp = loader.cordapps.first() val cordapp = loader.cordapps.first()
assertThat(cordapp.targetPlatformVersion).isEqualTo(3) assertThat(cordapp.targetPlatformVersion).isEqualTo(3)
assertThat(cordapp.minimumPlatformVersion).isEqualTo(2) assertThat(cordapp.minimumPlatformVersion).isEqualTo(2)
@ -132,8 +133,8 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader sets target version to min version if target version is not specified`() { fun `cordapp classloader sets target version to min version if target version is not specified`() {
// load jar with minVersion but not targetVersion in manifest // load jar with minVersion but not targetVersion in manifest
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN)
// exclude the core cordapp // exclude the core cordapp
val cordapp = loader.cordapps.first() val cordapp = loader.cordapps.first()
assertThat(cordapp.targetPlatformVersion).isEqualTo(2) assertThat(cordapp.targetPlatformVersion).isEqualTo(2)
@ -142,8 +143,8 @@ class JarScanningCordappLoaderTest {
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() { fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!.toPath()
val cordappLoader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1)) val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1))
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy { assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
cordappLoader.cordapps cordappLoader.cordapps
} }
@ -151,29 +152,29 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader does load apps when their min platform version is less than the platform version`() { fun `cordapp classloader does load apps when their min platform version is less than the platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000)) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1000))
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() { fun `cordapp classloader does load apps when their min platform version is equal to the platform version`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-target-3.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2))
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
} }
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader loads app signed by allowed certificate`() { fun `cordapp classloader loads app signed by allowed certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = emptyList()) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = emptyList())
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
} }
@Test(timeout = 300_000) @Test(timeout = 300_000)
fun `cordapp classloader does not load app signed by blacklisted certificate`() { fun `cordapp classloader does not load app signed by blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!.toPath()
val cordappLoader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) val cordappLoader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy { assertThatExceptionOfType(InvalidCordappException::class.java).isThrownBy {
cordappLoader.cordapps cordappLoader.cordapps
} }
@ -181,8 +182,8 @@ class JarScanningCordappLoaderTest {
@Test(timeout=300_000) @Test(timeout=300_000)
fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() { fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() {
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!! val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!!.toPath()
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES) val loader = JarScanningCordappLoader.fromJarUrls(setOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
assertThat(loader.cordapps).hasSize(1) assertThat(loader.cordapps).hasSize(1)
} }
} }

View File

@ -128,7 +128,7 @@ open class MockServices private constructor(
) : ServiceHub { ) : ServiceHub {
companion object { companion object {
private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader { private fun cordappLoaderForPackages(packages: Iterable<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).map { it.jarFile.toUri().toURL() }, versionInfo) return JarScanningCordappLoader.fromJarUrls(cordappsForPackages(packages).mapToSet { it.jarFile }, versionInfo)
} }
/** /**

View File

@ -3,10 +3,11 @@ apply plugin: 'org.jetbrains.kotlin.jvm'
description 'Utilities needed for smoke tests in Corda' description 'Utilities needed for smoke tests in Corda'
dependencies { dependencies {
api project(':test-common')
// Smoke tests do NOT have any Node code on the classpath! // Smoke tests do NOT have any Node code on the classpath!
implementation project(':core') implementation project(':core')
implementation project(':node-api') implementation project(':node-api')
implementation project(':test-common')
implementation project(':client:rpc') implementation project(':client:rpc')
implementation "com.google.guava:guava:$guava_version" implementation "com.google.guava:guava:$guava_version"

View File

@ -9,6 +9,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.toConfig import net.corda.nodeapi.internal.config.toConfig
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.absolutePathString
class NodeParams @JvmOverloads constructor( class NodeParams @JvmOverloads constructor(
val legalName: CordaX500Name, val legalName: CordaX500Name,
@ -17,6 +18,7 @@ class NodeParams @JvmOverloads constructor(
val rpcAdminPort: Int, val rpcAdminPort: Int,
val users: List<User>, val users: List<User>,
val cordappJars: List<Path> = emptyList(), val cordappJars: List<Path> = emptyList(),
val jarDirs: List<Path> = emptyList(),
val clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, val clientRpcConfig: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT,
val devMode: Boolean = true, val devMode: Boolean = true,
val version: String? = null val version: String? = null
@ -37,6 +39,7 @@ class NodeParams @JvmOverloads constructor(
.root()) .root())
.withValue("rpcUsers", valueFor(users.map { it.toConfig().root().unwrapped() }.toList())) .withValue("rpcUsers", valueFor(users.map { it.toConfig().root().unwrapped() }.toList()))
.withValue("useTestClock", valueFor(true)) .withValue("useTestClock", valueFor(true))
.withValue("jarDirs", valueFor(jarDirs.map(Path::absolutePathString)))
.withValue("devMode", valueFor(devMode)) .withValue("devMode", valueFor(devMode))
return if (isNotary) { return if (isNotary) {
config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true))) config.withValue("notary", ConfigValueFactory.fromMap(mapOf("validating" to true)))