diff --git a/core/src/main/kotlin/core/Utils.kt b/core/src/main/kotlin/core/Utils.kt index f735723451..5d140972e9 100644 --- a/core/src/main/kotlin/core/Utils.kt +++ b/core/src/main/kotlin/core/Utils.kt @@ -8,10 +8,12 @@ package core +import com.google.common.io.ByteStreams import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture import org.slf4j.Logger +import java.io.BufferedInputStream import java.io.InputStream import java.nio.file.Files import java.nio.file.Path @@ -21,6 +23,7 @@ import java.time.temporal.Temporal import java.util.concurrent.Executor import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock +import java.util.zip.ZipInputStream import kotlin.concurrent.withLock import kotlin.reflect.KProperty @@ -121,4 +124,32 @@ class TransientProperty(private val initializer: () -> T) { v = initializer() return v!! } -} \ No newline at end of file +} + +/** + * Given a path to a zip file, extracts it to the given directory. + */ +fun extractZipFile(zipPath: Path, toPath: Path) { + if (!Files.exists(toPath)) + Files.createDirectories(toPath) + + ZipInputStream(BufferedInputStream(Files.newInputStream(zipPath))).use { zip -> + while (true) { + val e = zip.nextEntry ?: break + val outPath = toPath.resolve(e.name) + + // Security checks: we should reject a zip that contains tricksy paths that try to escape toPath. + if (!outPath.normalize().startsWith(toPath)) + throw IllegalStateException("ZIP contained a path that resolved incorrectly: ${e.name}") + + if (e.isDirectory) { + Files.createDirectories(outPath) + continue + } + Files.newOutputStream(outPath).use { out -> + ByteStreams.copy(zip, out) + } + zip.closeEntry() + } + } +} diff --git a/src/main/kotlin/core/node/NodeAttachmentStorage.kt b/src/main/kotlin/core/node/NodeAttachmentStorage.kt index 182218ef5e..a141b1f626 100644 --- a/src/main/kotlin/core/node/NodeAttachmentStorage.kt +++ b/src/main/kotlin/core/node/NodeAttachmentStorage.kt @@ -15,11 +15,13 @@ import com.google.common.io.CountingInputStream import core.Attachment import core.AttachmentStorage import core.crypto.SecureHash +import core.extractZipFile import core.utilities.loggerFor import java.io.FilterInputStream import java.io.InputStream import java.nio.file.Files import java.nio.file.Path +import java.nio.file.Paths import java.nio.file.StandardCopyOption import java.util.* import java.util.jar.JarInputStream @@ -35,6 +37,13 @@ class NodeAttachmentStorage(val storePath: Path) : AttachmentStorage { @VisibleForTesting var checkAttachmentsOnLoad = true + /** + * If true, newly inserted attachments will be unzipped to a subdirectory of the [storePath]. This is intended for + * human browsing convenience: the attachment itself will still be the file (that is, edits to the extracted directory + * will not have any effect). + */ + @Volatile var automaticallyExtractAttachments = false + init { require(Files.isDirectory(storePath)) { "$storePath must be a directory" } } @@ -102,14 +111,29 @@ class NodeAttachmentStorage(val storePath: Path) : AttachmentStorage { Files.deleteIfExists(tmp) } log.info("Stored new attachment $id") + if (automaticallyExtractAttachments) { + val extractTo = storePath.resolve("${id}.jar") + try { + Files.createDirectory(extractTo) + extractZipFile(finalPath, extractTo) + } catch(e: Exception) { + log.error("Failed to extract attachment jar $id, ", e) + // TODO: Delete the extractTo directory here. + } + } return id } private fun checkIsAValidJAR(path: Path) { // Just iterate over the entries with verification enabled: should be good enough to catch mistakes. JarInputStream(Files.newInputStream(path), true).use { stream -> - var cursor = stream.nextJarEntry - while (cursor != null) cursor = stream.nextJarEntry + while (true) { + val cursor = stream.nextJarEntry ?: break + val entryPath = Paths.get(cursor.name) + // Security check to stop zips trying to escape their rightful place. + if (entryPath.isAbsolute || entryPath.normalize() != entryPath) + throw IllegalArgumentException("Path is either absolute or non-normalised: $entryPath") + } } } }