mirror of
https://github.com/corda/corda.git
synced 2025-02-22 18:12:53 +00:00
Fix downloading attachments from WebServer. (#615)
* Fix /attachments endpoint on WebServer, and update demo to use it. * Add @Throws statements to servlet methods. * Ensure target entry is not a directory. * Simplify, because JarInputStream verifies signatures by default. * Move JarInputStream.extractFile() function into core. * Don't close the output stream automatically as it commits our response.
This commit is contained in:
parent
ce8e40df50
commit
780f93e625
@ -10,6 +10,7 @@ import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
@ -496,20 +497,7 @@ interface Attachment : NamedByHash {
|
||||
*
|
||||
* @throws FileNotFoundException if the given path doesn't exist in the attachment.
|
||||
*/
|
||||
fun extractFile(path: String, outputTo: OutputStream) {
|
||||
val p = path.toLowerCase().split('\\', '/')
|
||||
openAsJAR().use { jar ->
|
||||
while (true) {
|
||||
val e = jar.nextJarEntry ?: break
|
||||
if (e.name.toLowerCase().split('\\', '/') == p) {
|
||||
jar.copyTo(outputTo)
|
||||
return
|
||||
}
|
||||
jar.closeEntry()
|
||||
}
|
||||
}
|
||||
throw FileNotFoundException(path)
|
||||
}
|
||||
fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) }
|
||||
}
|
||||
|
||||
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
@ -529,3 +517,17 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
override fun hashCode() = id.hashCode()
|
||||
override fun toString() = "${javaClass.simpleName}(id=$id)"
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
|
||||
val p = path.toLowerCase().split('\\', '/')
|
||||
while (true) {
|
||||
val e = nextJarEntry ?: break
|
||||
if (!e.isDirectory && e.name.toLowerCase().split('\\', '/') == p) {
|
||||
copyTo(outputTo)
|
||||
return
|
||||
}
|
||||
closeEntry()
|
||||
}
|
||||
throw FileNotFoundException(path)
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
advertisedServices = []
|
||||
p2pPort 10008
|
||||
rpcPort 10009
|
||||
webPort 10010
|
||||
cordapps = []
|
||||
rpcUsers = ext.rpcUsers
|
||||
}
|
||||
|
@ -19,9 +19,14 @@ import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.flows.FinalityFlow
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.security.PublicKey
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.servlet.http.HttpServletResponse.SC_OK
|
||||
import javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION
|
||||
import javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal enum class Role {
|
||||
SENDER,
|
||||
@ -73,7 +78,7 @@ fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256)
|
||||
if (!rpc.attachmentExists(hash)) {
|
||||
inputStream.use {
|
||||
val id = rpc.uploadAttachment(it)
|
||||
assertEquals(hash, id)
|
||||
require(hash == id) { "Id was '$id' instead of '$hash'" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +107,29 @@ fun recipient(rpc: CordaRPCOps) {
|
||||
if (wtx.outputs.isNotEmpty()) {
|
||||
val state = wtx.outputs.map { it.data }.filterIsInstance<AttachmentContract.State>().single()
|
||||
require(rpc.attachmentExists(state.hash))
|
||||
|
||||
// Download the attachment via the Web endpoint.
|
||||
val connection = URL("http://localhost:10010/attachments/${state.hash}").openConnection() as HttpURLConnection
|
||||
try {
|
||||
require(connection.responseCode == SC_OK) { "HTTP status code was ${connection.responseCode}" }
|
||||
require(connection.contentType == APPLICATION_OCTET_STREAM) { "Content-Type header was ${connection.contentType}" }
|
||||
require(connection.contentLength > 1024) { "Attachment contains only ${connection.contentLength} bytes" }
|
||||
require(connection.getHeaderField(CONTENT_DISPOSITION) == "attachment; filename=\"${state.hash}.zip\"") {
|
||||
"Content-Disposition header was ${connection.getHeaderField(CONTENT_DISPOSITION)}"
|
||||
}
|
||||
|
||||
// Write out the entries inside this jar.
|
||||
println("Attachment JAR contains these entries:")
|
||||
JarInputStream(connection.inputStream).use { it ->
|
||||
while (true) {
|
||||
val e = it.nextJarEntry ?: break
|
||||
println("Entry> ${e.name}")
|
||||
it.closeEntry()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}")
|
||||
} else {
|
||||
println("Error: no output state found in ${wtx.id}")
|
||||
|
@ -1,12 +1,17 @@
|
||||
package net.corda.webserver.servlets
|
||||
|
||||
import net.corda.core.contracts.extractFile
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.services.StorageService
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.ws.rs.core.HttpHeaders
|
||||
import javax.ws.rs.core.MediaType
|
||||
|
||||
/**
|
||||
* Allows the node administrator to either download full attachment zips, or individual files within those zips.
|
||||
@ -17,11 +22,12 @@ import javax.servlet.http.HttpServletResponse
|
||||
* Files are always forced to be downloads, they may not be embedded into web pages for security reasons.
|
||||
*
|
||||
* TODO: See if there's a way to prevent access by JavaScript.
|
||||
* TODO: Provide an endpoint that exposes attachment file listings, to make attachments browseable.
|
||||
* TODO: Provide an endpoint that exposes attachment file listings, to make attachments browsable.
|
||||
*/
|
||||
class AttachmentDownloadServlet : HttpServlet() {
|
||||
private val log = loggerFor<AttachmentDownloadServlet>()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
val reqPath = req.pathInfo?.substring(1)
|
||||
if (reqPath == null) {
|
||||
@ -31,21 +37,23 @@ class AttachmentDownloadServlet : HttpServlet() {
|
||||
|
||||
try {
|
||||
val hash = SecureHash.parse(reqPath.substringBefore('/'))
|
||||
val storage = servletContext.getAttribute("storage") as StorageService
|
||||
val attachment = storage.attachments.openAttachment(hash) ?: throw FileNotFoundException()
|
||||
val rpc = servletContext.getAttribute("rpc") as CordaRPCOps
|
||||
val attachment = rpc.openAttachment(hash)
|
||||
|
||||
// Don't allow case sensitive matches inside the jar, it'd just be confusing.
|
||||
val subPath = reqPath.substringAfter('/', missingDelimiterValue = "").toLowerCase()
|
||||
|
||||
resp.contentType = "application/octet-stream"
|
||||
if (subPath == "") {
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"$hash.zip\"")
|
||||
attachment.open().use { it.copyTo(resp.outputStream) }
|
||||
resp.contentType = MediaType.APPLICATION_OCTET_STREAM
|
||||
if (subPath.isEmpty()) {
|
||||
resp.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"$hash.zip\"")
|
||||
attachment.use { it.copyTo(resp.outputStream) }
|
||||
} else {
|
||||
val filename = subPath.split('/').last()
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"$filename\"")
|
||||
attachment.extractFile(subPath, resp.outputStream)
|
||||
resp.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"$filename\"")
|
||||
JarInputStream(attachment).use { it.extractFile(subPath, resp.outputStream) }
|
||||
}
|
||||
|
||||
// Closing the output stream commits our response. We cannot change the status code after this.
|
||||
resp.outputStream.close()
|
||||
} catch(e: FileNotFoundException) {
|
||||
log.warn("404 Not Found whilst trying to handle attachment download request for ${servletContext.contextPath}/$reqPath")
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import org.glassfish.jersey.server.model.Resource
|
||||
import org.glassfish.jersey.server.model.ResourceMethod
|
||||
import java.io.IOException
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
@ -16,6 +17,7 @@ import javax.servlet.http.HttpServletResponse
|
||||
*/
|
||||
class CorDappInfoServlet(val plugins: List<CordaPluginRegistry>, val rpc: CordaRPCOps): HttpServlet() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
resp.writer.appendHTML().html {
|
||||
head {
|
||||
|
@ -3,6 +3,7 @@ package net.corda.webserver.servlets
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.apache.commons.fileupload.servlet.ServletFileUpload
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
@ -14,6 +15,7 @@ import javax.servlet.http.HttpServletResponse
|
||||
class DataUploadServlet : HttpServlet() {
|
||||
private val log = loggerFor<DataUploadServlet>()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
@Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated.
|
||||
val isMultipart = ServletFileUpload.isMultipartContent(req)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.webserver.servlets
|
||||
|
||||
import java.io.IOException
|
||||
import javax.ws.rs.container.ContainerRequestContext
|
||||
import javax.ws.rs.container.ContainerResponseContext
|
||||
import javax.ws.rs.container.ContainerResponseFilter
|
||||
@ -10,6 +11,7 @@ import javax.ws.rs.ext.Provider
|
||||
*/
|
||||
@Provider
|
||||
class ResponseFilter : ContainerResponseFilter {
|
||||
@Throws(IOException::class)
|
||||
override fun filter(requestContext: ContainerRequestContext, responseContext: ContainerResponseContext) {
|
||||
val headers = responseContext.headers
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user