nuts-foundation/nuts-discovery

View on GitHub
src/main/kotlin/nl/nuts/discovery/api/NetworkMapApi.kt

Summary

Maintainability
B
4 hrs
Test Coverage
/*
 *     Nuts discovery service for Corda network creation
 *     Copyright (C) 2019 Nuts community
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package nl.nuts.discovery.api

import net.corda.core.crypto.SecureHash
import net.corda.core.internal.CertRole
import net.corda.core.internal.readObject
import net.corda.core.serialization.serialize
import net.corda.nodeapi.internal.SignedNodeInfo
import net.corda.nodeapi.internal.network.NetworkMap
import net.corda.nodeapi.internal.network.SignedNetworkParameters
import nl.nuts.discovery.service.CertificateAndKeyService
import nl.nuts.discovery.service.NetworkParametersService
import nl.nuts.discovery.store.NetworkParametersRepository
import nl.nuts.discovery.store.NodeRepository
import nl.nuts.discovery.store.entity.NetworkParameters
import nl.nuts.discovery.store.entity.Node
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.CacheControl
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.util.concurrent.TimeUnit

/**
 * This api should be behind TLS two-way ssl using the Corda TLS keystore....
 *
 * Taken from net.corda.testing.node.internal.network.NetworkMapServer
 */
@RestController
@RequestMapping("/network-map", produces = arrayOf("*/*"), consumes = arrayOf("*/*"))
class NetworkMapApi {
    val logger = LoggerFactory.getLogger(this.javaClass)

    @Qualifier("customNodeRepository")
    @Autowired
    lateinit var nodeRepository: NodeRepository

    @Autowired
    lateinit var certificateAndKeyService: CertificateAndKeyService

    @Autowired
    lateinit var networkParametersService: NetworkParametersService

    @Autowired
    lateinit var networkParametersRepository: NetworkParametersRepository

    /**
     * Accept NodeInfo from connecting nodes.
     */
    @RequestMapping("/publish", method = arrayOf(RequestMethod.POST), produces = arrayOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
    fun publishNodeInfo(@RequestBody input: ByteArray): ResponseEntity<ByteArray> {
        try {
            val signedNodeInfo = ByteArrayInputStream(input).readObject<SignedNodeInfo>()

            //verify
            val pAndcert = signedNodeInfo.verified().legalIdentitiesAndCerts.firstOrNull()?: throw IllegalArgumentException("signedNodeInfo must have legal identity")
            val node = Node.fromNodeInfo(signedNodeInfo)
            logger.info("received a publish request for legalIdentities: {} with role", node.name, CertRole.extract(pAndcert.certificate))

            if (node.notary == true) {
                logger.debug("new notary detected")
                networkParametersService.updateNetworkParams(node)
            } else {
                nodeRepository.save(node)
            }

        } catch (e: Exception) {
            logger.error(e.message, e)
            throw e
        }

        return ResponseEntity.ok("".toByteArray())
    }

    /**
     * Currently not supported
     */
    @RequestMapping("/ack-parameters", method = arrayOf(RequestMethod.POST), consumes = arrayOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
    fun ackNetworkParameters(@RequestBody input: InputStream): ResponseEntity<Any> {

        return ResponseEntity.ok().build()
    }

    /**
     * Retrieve the global networkMap signed with the network map key.
     */
    @RequestMapping("", method = arrayOf(RequestMethod.GET), produces = arrayOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
    fun getGlobalNetworkMap(): ResponseEntity<ByteArray> {

        logger.debug("received network-map request")

        try {
            val nodeListHashes = nodeRepository.findAll().map { SecureHash.parse(it.hash) }
            val latestNetworkParams = networkParametersRepository.findFirstByOrderByIdDesc() ?: return ResponseEntity.notFound().build()
            val networkMap = NetworkMap(nodeListHashes, SecureHash.parse(latestNetworkParams.hash!!), null)
            val signedNetworkMap = certificateAndKeyService.signNetworkMap(networkMap)

            logger.debug("returned networkMap with params hash: {}", latestNetworkParams.hash)

            return ResponseEntity
                .ok()
                .cacheControl(CacheControl.maxAge(10, TimeUnit.SECONDS))
                .body(signedNetworkMap.serialize().bytes)
        } catch (e: Exception) {
            logger.error(e.message, e)
            throw e
        }
    }

    /**
     * Currently not implemented
     */
    @RequestMapping("/{var}", method = arrayOf(RequestMethod.GET), produces = arrayOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
    fun getPrivateNetworkMap(@PathVariable("var") extraUUID: String): ResponseEntity<InputStream> {

        return ResponseEntity.ok("".byteInputStream(Charsets.UTF_8))
    }

    /**
     * Retrieve the NodeInfo object published by the node. Index is the secure hash of the nodeInfo object.
     */
    @RequestMapping("node-info/{var}", method = arrayOf(RequestMethod.GET), produces = arrayOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
    fun getNodeInfo(@PathVariable("var") nodeInfoHash: String): ResponseEntity<ByteArray> {

        logger.debug("received node-info request for {}", nodeInfoHash)

        val node = nodeRepository.findByHash(nodeInfoHash)

        return if (node != null) {
            ResponseEntity.ok(node.toSignedNodeInfo().serialize().bytes)
        } else {
            ResponseEntity.status(HttpStatus.NOT_FOUND).build()
        }
    }

    /**
     * Get the latest network parameters.
     * Right now it returns the latest params and doesn't support any others yet.
     */
    @RequestMapping("network-parameters/latest", method = arrayOf(RequestMethod.GET), produces = arrayOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
    fun getLatestNetworkParameter(): ResponseEntity<ByteArray> {
        logger.debug("received latest network-parameters request")

        val np = networkParametersRepository.findFirstByOrderByIdDesc() ?: return ResponseEntity.notFound().build()

        return ResponseEntity.ok(signedNetworkParams(np).serialize().bytes)
    }

    /**
     * Get the network parameters based on its hash.
     */
    @RequestMapping("network-parameters/{var}", method = arrayOf(RequestMethod.GET), produces = arrayOf(MediaType.APPLICATION_OCTET_STREAM_VALUE))
    fun getNetworkParameter(@PathVariable("var") hash: String): ResponseEntity<ByteArray> {
        logger.debug("received network-parameters request for {}", hash)

        val np = networkParametersRepository.findByHash(hash) ?: return ResponseEntity.notFound().build()

        return ResponseEntity.ok(signedNetworkParams(np).serialize().bytes)
    }

    private fun signedNetworkParams(np: NetworkParameters): SignedNetworkParameters {
        return certificateAndKeyService.signNetworkParams(networkParametersService.cordaNetworkParameters(np))
    }
}