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
This commit is contained in:
Patrick Kuo 2018-02-01 13:38:25 +00:00 committed by GitHub
parent 641cecaf70
commit dca8699e7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 44 additions and 23 deletions

View File

@ -57,7 +57,7 @@ Allowed parameters are:
:rootStorePath: Path for the root keystore :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 :signInterval: How often to sign the network map in seconds

View File

@ -18,6 +18,7 @@ import net.corda.nodeapi.internal.network.NetworkParameters
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import java.io.Closeable import java.io.Closeable
import java.net.URI import java.net.URI
import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -35,7 +36,7 @@ class NetworkManagementServer : Closeable {
try { try {
closeAction() closeAction()
} catch (e: Exception) { } 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) scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
closeActions += scheduledExecutor::shutdown closeActions += scheduledExecutor::shutdown
return RegistrationWebService(requestProcessor) return RegistrationWebService(requestProcessor, Duration.ofMillis(config.approveInterval))
} }
fun start(hostAndPort: NetworkHostAndPort, fun start(hostAndPort: NetworkHostAndPort,

View File

@ -8,6 +8,7 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.time.Duration
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import javax.servlet.http.HttpServletRequest 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. * Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results.
*/ */
@Path("certificate") @Path("certificate")
class RegistrationWebService(private val csrHandler: CsrHandler) { class RegistrationWebService(private val csrHandler: CsrHandler, private val clientPollInterval: Duration) {
@Context lateinit var request: HttpServletRequest @Context lateinit var request: HttpServletRequest
/** /**
* Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificateRequestStorage] for approval. * 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") .type("application/zip")
.header("Content-Disposition", "attachment; filename=\"certificates.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) is CertificateResponse.Unauthorised -> status(UNAUTHORIZED).entity(response.message)
}.build() }.build()
} }

View File

@ -9,6 +9,8 @@ import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort 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.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory
@ -43,6 +45,7 @@ class RegistrationWebServiceTest : TestBase() {
private lateinit var webServer: NetworkManagementWebServer private lateinit var webServer: NetworkManagementWebServer
private lateinit var rootCaCert: X509Certificate private lateinit var rootCaCert: X509Certificate
private lateinit var intermediateCa: CertificateAndKeyPair private lateinit var intermediateCa: CertificateAndKeyPair
private val pollInterval = 10.seconds
@Before @Before
fun init() { fun init() {
@ -52,7 +55,7 @@ class RegistrationWebServiceTest : TestBase() {
} }
private fun startSigningServer(csrHandler: CsrHandler) { private fun startSigningServer(csrHandler: CsrHandler) {
webServer = NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler)) webServer = NetworkManagementWebServer(NetworkHostAndPort("localhost", 0), RegistrationWebService(csrHandler, pollInterval))
webServer.start() webServer.start()
} }
@ -115,7 +118,9 @@ class RegistrationWebServiceTest : TestBase() {
} }
startSigningServer(requestProcessor) startSigningServer(requestProcessor)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
val response = pollForResponse(id)
assertEquals(pollInterval, (response as PollResponse.NotReady).pollInterval.seconds)
requestProcessor.processRequests() requestProcessor.processRequests()
@ -163,7 +168,9 @@ class RegistrationWebServiceTest : TestBase() {
} }
startSigningServer(storage) startSigningServer(storage)
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) val response = pollForResponse(id)
assertEquals(pollInterval, (response as PollResponse.NotReady).pollInterval.seconds)
storage.processRequests() storage.processRequests()
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
@ -218,14 +225,14 @@ class RegistrationWebServiceTest : TestBase() {
} }
PollResponse.Ready(certificates) 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)) HTTP_UNAUTHORIZED -> PollResponse.Unauthorised(IOUtils.toString(conn.errorStream, UTF_8))
else -> throw IOException("Cannot connect to Certificate Signing Server, HTTP response code : ${conn.responseCode}") else -> throw IOException("Cannot connect to Certificate Signing Server, HTTP response code : ${conn.responseCode}")
} }
} }
private interface PollResponse { private interface PollResponse {
object NotReady : PollResponse data class NotReady(val pollInterval: Int) : PollResponse
data class Ready(val certChain: List<X509Certificate>) : PollResponse data class Ready(val certChain: List<X509Certificate>) : PollResponse
data class Unauthorised(val message: String) : PollResponse data class Unauthorised(val message: String) : PollResponse
} }

View File

@ -14,6 +14,7 @@ import net.corda.core.utilities.seconds
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.api.NetworkMapCacheInternal
import net.corda.node.utilities.NamedThreadFactory import net.corda.node.utilities.NamedThreadFactory
import net.corda.node.utilities.registration.cacheControl
import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NetworkMap import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.NetworkParameters import net.corda.nodeapi.internal.network.NetworkParameters
@ -54,7 +55,7 @@ class NetworkMapClient(compatibilityZoneURL: URL, private val trustedRoot: X509C
val connection = networkMapUrl.openHttpConnection() val connection = networkMapUrl.openHttpConnection()
val signedNetworkMap = connection.responseAs<SignedDataWithCert<NetworkMap>>() val signedNetworkMap = connection.responseAs<SignedDataWithCert<NetworkMap>>()
val networkMap = signedNetworkMap.verifiedNetworkMapCert(trustedRoot) 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")}" } 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) return NetworkMapResponse(networkMap, timeout)
} }

View File

@ -2,7 +2,10 @@ package net.corda.node.utilities.registration
import com.google.common.net.MediaType import com.google.common.net.MediaType
import net.corda.core.internal.openHttpConnection import net.corda.core.internal.openHttpConnection
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509CertificateFactory
import okhttp3.CacheControl
import okhttp3.Headers
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.io.IOException import java.io.IOException
@ -22,10 +25,13 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr
} }
@Throws(CertificateRequestException::class) @Throws(CertificateRequestException::class)
override fun retrieveCertificates(requestId: String): List<X509Certificate>? { override fun retrieveCertificates(requestId: String): CertificateResponse {
// Poll server to download the signed certificate once request has been approved. // Poll server to download the signed certificate once request has been approved.
val conn = URL("$registrationURL/$requestId").openHttpConnection() val conn = URL("$registrationURL/$requestId").openHttpConnection()
conn.requestMethod = "GET" 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) { return when (conn.responseCode) {
HTTP_OK -> ZipInputStream(conn.inputStream).use { HTTP_OK -> ZipInputStream(conn.inputStream).use {
@ -34,9 +40,9 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr
while (it.nextEntry != null) { while (it.nextEntry != null) {
certificates += factory.generateCertificate(it) 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}") HTTP_UNAUTHORIZED -> throw CertificateRequestException("Certificate signing request has been rejected: ${conn.errorMessage}")
else -> throwUnexpectedResponseCode(conn) else -> throwUnexpectedResponseCode(conn)
} }
@ -66,3 +72,5 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL) : NetworkRegistr
private val HttpURLConnection.errorMessage: String get() = IOUtils.toString(errorStream, charset) 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] }))

View File

@ -3,7 +3,6 @@ package net.corda.node.utilities.registration
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.utilities.seconds
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509KeyStore
@ -28,7 +27,6 @@ class NetworkRegistrationHelper(private val config: NodeConfiguration,
networkRootTrustStorePath: Path, networkRootTrustStorePath: Path,
networkRootTruststorePassword: String) { networkRootTruststorePassword: String) {
private companion object { private companion object {
val pollInterval = 10.seconds
const val SELF_SIGNED_PRIVATE_KEY = "Self Signed Private Key" 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, * Poll Certificate Signing Server for approved certificate,
* enter a slow polling loop if server return null. * enter a slow polling loop if server return null.
* @param requestId Certificate signing request ID. * @param requestId Certificate signing request ID.
* @return Map of certificate chain. * @return List of certificate chain.
*/ */
private fun pollServerForCertificates(requestId: String): List<X509Certificate> { private fun pollServerForCertificates(requestId: String): List<X509Certificate> {
println("Start polling server for certificate signing approval.") println("Start polling server for certificate signing approval.")
// Poll server to download the signed certificate once request has been approved. // Poll server to download the signed certificate once request has been approved.
var certificates = certService.retrieveCertificates(requestId) while (true) {
while (certificates == null) { val (pollInterval, certificates) = certService.retrieveCertificates(requestId)
if (certificates != null) {
return certificates
}
Thread.sleep(pollInterval.toMillis()) Thread.sleep(pollInterval.toMillis())
certificates = certService.retrieveCertificates(requestId)
} }
return certificates
} }
/** /**

View File

@ -4,6 +4,7 @@ import net.corda.core.CordaException
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Duration
interface NetworkRegistrationService { interface NetworkRegistrationService {
/** Submits a CSR to the signing service and returns an opaque request ID. */ /** 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. */ /** Poll Certificate Signing Server for the request and returns a chain of certificates if request has been approved, null otherwise. */
@Throws(CertificateRequestException::class) @Throws(CertificateRequestException::class)
fun retrieveCertificates(requestId: String): List<X509Certificate>? fun retrieveCertificates(requestId: String): CertificateResponse
} }
data class CertificateResponse(val pollInterval: Duration, val certificates: List<X509Certificate>?)
@CordaSerializable @CordaSerializable
class CertificateRequestException(message: String) : CordaException(message) class CertificateRequestException(message: String) : CordaException(message)

View File

@ -12,6 +12,7 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories import net.corda.core.internal.createDirectories
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.internal.x500Name import net.corda.core.internal.x500Name
import net.corda.core.utilities.seconds
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509KeyStore
@ -158,7 +159,7 @@ class NetworkRegistrationHelperTest {
private fun createRegistrationHelper(response: List<X509Certificate>): NetworkRegistrationHelper { private fun createRegistrationHelper(response: List<X509Certificate>): NetworkRegistrationHelper {
val certService = rigorousMock<NetworkRegistrationService>().also { val certService = rigorousMock<NetworkRegistrationService>().also {
doReturn(requestId).whenever(it).submitRequest(any()) 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) return NetworkRegistrationHelper(config, certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword)
} }