From dca8699e7c3f0c40f533bbefb85f1dbe90b2a873 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Thu, 1 Feb 2018 13:38:25 +0000 Subject: [PATCH] set network registration poll interval via http cache control header (#434) * set network registration poll interval via http cache control header from the server side * default poll interval to 10 seconds if cache header not found * address PR issues * address PR issues --- docs/source/running-doorman.rst | 2 +- .../doorman/NetworkManagementServer.kt | 5 +++-- .../webservice/RegistrationWebService.kt | 5 +++-- .../webservice/RegistrationWebServiceTest.kt | 17 ++++++++++++----- .../node/services/network/NetworkMapClient.kt | 3 ++- .../HTTPNetworkRegistrationService.kt | 14 +++++++++++--- .../registration/NetworkRegistrationHelper.kt | 13 ++++++------- .../registration/NetworkRegistrationService.kt | 5 ++++- .../NetworkRegistrationHelperTest.kt | 3 ++- 9 files changed, 44 insertions(+), 23 deletions(-) diff --git a/docs/source/running-doorman.rst b/docs/source/running-doorman.rst index 5915c23965..ddb69dd735 100644 --- a/docs/source/running-doorman.rst +++ b/docs/source/running-doorman.rst @@ -57,7 +57,7 @@ Allowed parameters are: :rootStorePath: Path for the root keystore -:approveInterval: How often to process Jira approved requests in seconds +:approveInterval: How often to process Jira approved requests in seconds. This will also be added to the http header, to be use as poll interval in Corda client. :signInterval: How often to sign the network map in seconds diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt index 919ca722be..dc829e64b3 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt @@ -18,6 +18,7 @@ import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.persistence.CordaPersistence import java.io.Closeable import java.net.URI +import java.time.Duration import java.time.Instant import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -35,7 +36,7 @@ class NetworkManagementServer : Closeable { try { closeAction() } catch (e: Exception) { - logger.warn("Discregarding exception thrown during close", e) + logger.warn("Disregarding exception thrown during close", e) } } } @@ -111,7 +112,7 @@ class NetworkManagementServer : Closeable { scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS) closeActions += scheduledExecutor::shutdown - return RegistrationWebService(requestProcessor) + return RegistrationWebService(requestProcessor, Duration.ofMillis(config.approveInterval)) } fun start(hostAndPort: NetworkHostAndPort, diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt index 278bef3e63..7478cabc67 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebService.kt @@ -8,6 +8,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import java.io.ByteArrayOutputStream import java.io.InputStream +import java.time.Duration import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import javax.servlet.http.HttpServletRequest @@ -22,7 +23,7 @@ import javax.ws.rs.core.Response.Status.UNAUTHORIZED * Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results. */ @Path("certificate") -class RegistrationWebService(private val csrHandler: CsrHandler) { +class RegistrationWebService(private val csrHandler: CsrHandler, private val clientPollInterval: Duration) { @Context lateinit var request: HttpServletRequest /** * Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificateRequestStorage] for approval. @@ -63,7 +64,7 @@ class RegistrationWebService(private val csrHandler: CsrHandler) { .type("application/zip") .header("Content-Disposition", "attachment; filename=\"certificates.zip\"") } - is CertificateResponse.NotReady -> noContent() + is CertificateResponse.NotReady -> noContent().header("Cache-Control", "max-age=${clientPollInterval.seconds}") is CertificateResponse.Unauthorised -> status(UNAUTHORIZED).entity(response.message) }.build() } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt index d17ec4dbf3..d74a3ff4af 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt @@ -9,6 +9,8 @@ import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.seconds +import net.corda.node.utilities.registration.cacheControl import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory @@ -43,6 +45,7 @@ class RegistrationWebServiceTest : TestBase() { private lateinit var webServer: NetworkManagementWebServer private lateinit var rootCaCert: X509Certificate private lateinit var intermediateCa: CertificateAndKeyPair + private val pollInterval = 10.seconds @Before fun init() { @@ -52,7 +55,7 @@ class RegistrationWebServiceTest : TestBase() { } private fun startSigningServer(csrHandler: CsrHandler) { - webServer = NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler)) + webServer = NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, pollInterval)) webServer.start() } @@ -115,7 +118,9 @@ class RegistrationWebServiceTest : TestBase() { } startSigningServer(requestProcessor) - assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) + + val response = pollForResponse(id) + assertEquals(pollInterval, (response as PollResponse.NotReady).pollInterval.seconds) requestProcessor.processRequests() @@ -163,7 +168,9 @@ class RegistrationWebServiceTest : TestBase() { } startSigningServer(storage) - assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) + val response = pollForResponse(id) + assertEquals(pollInterval, (response as PollResponse.NotReady).pollInterval.seconds) + storage.processRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain @@ -218,14 +225,14 @@ class RegistrationWebServiceTest : TestBase() { } PollResponse.Ready(certificates) } - HTTP_NO_CONTENT -> PollResponse.NotReady + HTTP_NO_CONTENT -> PollResponse.NotReady(conn.cacheControl().maxAgeSeconds()) HTTP_UNAUTHORIZED -> PollResponse.Unauthorised(IOUtils.toString(conn.errorStream, UTF_8)) else -> throw IOException("Cannot connect to Certificate Signing Server, HTTP response code : ${conn.responseCode}") } } private interface PollResponse { - object NotReady : PollResponse + data class NotReady(val pollInterval: Int) : PollResponse data class Ready(val certChain: List) : PollResponse data class Unauthorised(val message: String) : PollResponse } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt index 9c12056315..39b4a2eb31 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapClient.kt @@ -14,6 +14,7 @@ import net.corda.core.utilities.seconds import net.corda.core.utilities.trace import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NamedThreadFactory +import net.corda.node.utilities.registration.cacheControl import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkParameters @@ -54,7 +55,7 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C val connection = networkMapUrl.openHttpConnection() val signedNetworkMap = connection.responseAs>() val networkMap = signedNetworkMap.verifiedNetworkMapCert(trustedRoot) - val timeout = CacheControl.parse(Headers.of(connection.headerFields.filterKeys { it != null }.mapValues { it.value[0] })).maxAgeSeconds().seconds + val timeout = connection.cacheControl().maxAgeSeconds().seconds logger.trace { "Fetched network map update from $networkMapUrl successfully, retrieved ${networkMap.nodeInfoHashes.size} node info hashes. Node Info hashes: ${networkMap.nodeInfoHashes.joinToString("\n")}" } return NetworkMapResponse(networkMap, timeout) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index ce6d974184..2a6236c10c 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -2,7 +2,10 @@ package net.corda.node.utilities.registration import com.google.common.net.MediaType import net.corda.core.internal.openHttpConnection +import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.crypto.X509CertificateFactory +import okhttp3.CacheControl +import okhttp3.Headers import org.apache.commons.io.IOUtils import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.io.IOException @@ -22,10 +25,13 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr } @Throws(CertificateRequestException::class) - override fun retrieveCertificates(requestId: String): List? { + override fun retrieveCertificates(requestId: String): CertificateResponse { // Poll server to download the signed certificate once request has been approved. val conn = URL("$registrationURL/$requestId").openHttpConnection() conn.requestMethod = "GET" + val maxAge = conn.cacheControl().maxAgeSeconds() + // Default poll interval to 10 seconds if not specified by the server, for backward compatibility. + val pollInterval = if (maxAge == -1) 10.seconds else maxAge.seconds return when (conn.responseCode) { HTTP_OK -> ZipInputStream(conn.inputStream).use { @@ -34,9 +40,9 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr while (it.nextEntry != null) { certificates += factory.generateCertificate(it) } - certificates + CertificateResponse(pollInterval, certificates) } - HTTP_NO_CONTENT -> null + HTTP_NO_CONTENT -> CertificateResponse(pollInterval, null) HTTP_UNAUTHORIZED -> throw CertificateRequestException("Certificate signing request has been rejected: ${conn.errorMessage}") else -> throwUnexpectedResponseCode(conn) } @@ -66,3 +72,5 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr private val HttpURLConnection.errorMessage: String get() = IOUtils.toString(errorStream, charset) } + +fun HttpURLConnection.cacheControl(): CacheControl = CacheControl.parse(Headers.of(headerFields.filterKeys { it != null }.mapValues { it.value[0] })) diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index a940a28a05..287f4b41c9 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -3,7 +3,6 @@ package net.corda.node.utilities.registration import net.corda.core.crypto.Crypto import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* -import net.corda.core.utilities.seconds import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore @@ -28,7 +27,6 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, networkRootTrustStorePath: Path, networkRootTruststorePassword: String) { private companion object { - val pollInterval = 10.seconds const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" } @@ -148,17 +146,18 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration, * Poll Certificate Signing Server for approved certificate, * enter a slow polling loop if server return null. * @param requestId Certificate signing request ID. - * @return Map of certificate chain. + * @return List of certificate chain. */ private fun pollServerForCertificates(requestId: String): List { println("Start polling server for certificate signing approval.") // Poll server to download the signed certificate once request has been approved. - var certificates = certService.retrieveCertificates(requestId) - while (certificates == null) { + while (true) { + val (pollInterval, certificates) = certService.retrieveCertificates(requestId) + if (certificates != null) { + return certificates + } Thread.sleep(pollInterval.toMillis()) - certificates = certService.retrieveCertificates(requestId) } - return certificates } /** diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt index beea4635d5..cafc16dd45 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationService.kt @@ -4,6 +4,7 @@ import net.corda.core.CordaException import net.corda.core.serialization.CordaSerializable import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.security.cert.X509Certificate +import java.time.Duration interface NetworkRegistrationService { /** Submits a CSR to the signing service and returns an opaque request ID. */ @@ -11,8 +12,10 @@ interface NetworkRegistrationService { /** Poll Certificate Signing Server for the request and returns a chain of certificates if request has been approved, null otherwise. */ @Throws(CertificateRequestException::class) - fun retrieveCertificates(requestId: String): List? + fun retrieveCertificates(requestId: String): CertificateResponse } +data class CertificateResponse(val pollInterval: Duration, val certificates: List?) + @CordaSerializable class CertificateRequestException(message: String) : CordaException(message) diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index e7e6775a5e..2371ab4437 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -12,6 +12,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.x500Name +import net.corda.core.utilities.seconds import net.corda.node.services.config.NodeConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore @@ -158,7 +159,7 @@ class NetworkRegistrationHelperTest { private fun createRegistrationHelper(response: List): NetworkRegistrationHelper { val certService = rigorousMock().also { doReturn(requestId).whenever(it).submitRequest(any()) - doReturn(response).whenever(it).retrieveCertificates(eq(requestId)) + doReturn(CertificateResponse(5.seconds, response)).whenever(it).retrieveCertificates(eq(requestId)) } return NetworkRegistrationHelper(config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword) }