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:
Chris Rankin 2017-05-03 13:21:26 +01:00 committed by GitHub
parent ce8e40df50
commit 780f93e625
7 changed files with 71 additions and 26 deletions

View File

@ -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)
}

View File

@ -70,6 +70,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
advertisedServices = []
p2pPort 10008
rpcPort 10009
webPort 10010
cordapps = []
rpcUsers = ext.rpcUsers
}

View File

@ -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}")

View File

@ -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")

View File

@ -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 {

View File

@ -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)

View File

@ -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