mirror of
https://github.com/corda/corda.git
synced 2025-02-01 08:48:09 +00:00
Allow download of attachments, and files within attachments, over HTTP.
This commit is contained in:
parent
a40886b63d
commit
d26b06c35c
@ -12,7 +12,9 @@ import core.crypto.SecureHash
|
||||
import core.crypto.toStringShort
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.serialization.serialize
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -169,4 +171,25 @@ class UnknownContractException : Exception()
|
||||
interface Attachment : NamedByHash {
|
||||
fun open(): InputStream
|
||||
fun openAsJAR() = JarInputStream(open())
|
||||
|
||||
/**
|
||||
* Finds the named file case insensitively and copies it to the output stream.
|
||||
*
|
||||
* @throws FileNotFoundException if the given path doesn't exist in the attachment.
|
||||
*/
|
||||
fun extractFile(path: String, outputTo: OutputStream) {
|
||||
val p = path.toLowerCase()
|
||||
openAsJAR().use { jar ->
|
||||
while (true) {
|
||||
val e = jar.nextJarEntry ?: break
|
||||
// TODO: Normalise path separators here for more platform independence, as zip doesn't mandate a type.
|
||||
if (e.name.toLowerCase() == p) {
|
||||
jar.copyTo(outputTo)
|
||||
return
|
||||
}
|
||||
jar.closeEntry()
|
||||
}
|
||||
}
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ sealed class SecureHash private constructor(bits: ByteArray) : OpaqueBytes(bits)
|
||||
fun parse(str: String) = BaseEncoding.base16().decode(str.toUpperCase()).let {
|
||||
when (it.size) {
|
||||
32 -> SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is not 32 bytes in base 16 (hex): $str")
|
||||
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ package core.node
|
||||
import com.google.common.net.HostAndPort
|
||||
import core.messaging.LegallyIdentifiableNode
|
||||
import core.messaging.MessagingService
|
||||
import core.node.servlets.AttachmentDownloadServlet
|
||||
import core.node.servlets.AttachmentUploadServlet
|
||||
import core.utilities.loggerFor
|
||||
import org.eclipse.jetty.server.Server
|
||||
@ -58,7 +59,8 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
||||
val server = Server(port)
|
||||
val handler = ServletContextHandler()
|
||||
handler.setAttribute("storage", storage)
|
||||
handler.addServlet(AttachmentUploadServlet::class.java, "/attachments/upload")
|
||||
handler.addServlet(AttachmentUploadServlet::class.java, "/attachments")
|
||||
handler.addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
||||
server.handler = handler
|
||||
server.start()
|
||||
return server
|
||||
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package core.node.servlets
|
||||
|
||||
import core.StorageService
|
||||
import core.crypto.SecureHash
|
||||
import core.utilities.loggerFor
|
||||
import java.io.FileNotFoundException
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
/**
|
||||
* Allows the node administrator to either download full attachment zips, or individual files within those zips.
|
||||
*
|
||||
* GET /attachments/123abcdef12121 -> download the zip identified by this hash
|
||||
* GET /attachments/123abcdef12121/foo.txt -> download that file specifically
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
class AttachmentDownloadServlet : HttpServlet() {
|
||||
private val log = loggerFor<AttachmentDownloadServlet>()
|
||||
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||
val reqPath = req.pathInfo?.substring(1)
|
||||
if (reqPath == null) {
|
||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val hash = SecureHash.parse(reqPath.substringBefore('/'))
|
||||
val storage = servletContext.getAttribute("storage") as StorageService
|
||||
val attachment = storage.attachments.openAttachment(hash) ?: throw FileNotFoundException()
|
||||
|
||||
// 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) }
|
||||
} else {
|
||||
val filename = subPath.split('/').last()
|
||||
resp.addHeader("Content-Disposition", "attachment; filename=\"$filename\"")
|
||||
attachment.extractFile(subPath, resp.outputStream)
|
||||
}
|
||||
resp.outputStream.close()
|
||||
} catch(e: FileNotFoundException) {
|
||||
log.warn("404 Not Found whilst trying to handle attachment download request for ${servletContext.contextPath}/$reqPath")
|
||||
resp.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user