Allow download of attachments, and files within attachments, over HTTP.

This commit is contained in:
Mike Hearn 2016-03-04 11:30:11 +01:00
parent a40886b63d
commit d26b06c35c
4 changed files with 91 additions and 2 deletions

View File

@ -12,7 +12,9 @@ import core.crypto.SecureHash
import core.crypto.toStringShort import core.crypto.toStringShort
import core.serialization.OpaqueBytes import core.serialization.OpaqueBytes
import core.serialization.serialize import core.serialization.serialize
import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.security.PublicKey import java.security.PublicKey
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
@ -169,4 +171,25 @@ class UnknownContractException : Exception()
interface Attachment : NamedByHash { interface Attachment : NamedByHash {
fun open(): InputStream fun open(): InputStream
fun openAsJAR() = JarInputStream(open()) 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()
}
} }

View File

@ -32,7 +32,7 @@ sealed class SecureHash private constructor(bits: ByteArray) : OpaqueBytes(bits)
fun parse(str: String) = BaseEncoding.base16().decode(str.toUpperCase()).let { fun parse(str: String) = BaseEncoding.base16().decode(str.toUpperCase()).let {
when (it.size) { when (it.size) {
32 -> SHA256(it) 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")
} }
} }

View File

@ -11,6 +11,7 @@ package core.node
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import core.messaging.LegallyIdentifiableNode import core.messaging.LegallyIdentifiableNode
import core.messaging.MessagingService import core.messaging.MessagingService
import core.node.servlets.AttachmentDownloadServlet
import core.node.servlets.AttachmentUploadServlet import core.node.servlets.AttachmentUploadServlet
import core.utilities.loggerFor import core.utilities.loggerFor
import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.Server
@ -58,7 +59,8 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
val server = Server(port) val server = Server(port)
val handler = ServletContextHandler() val handler = ServletContextHandler()
handler.setAttribute("storage", storage) 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.handler = handler
server.start() server.start()
return server return server

View File

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