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.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()
}
}

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

View File

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

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