blob: 4c6dbe264ff0b8968e075373609a302140dcdeb5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.sling.feature.cpconverter.handlers.slinginitialcontent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
import org.apache.sling.jcr.contentloader.PathEntry;
import org.jetbrains.annotations.NotNull;
/**
* Handles collecting the metadata for each sling initial content entry, to be used for extraction in another loop
*/
class SlingInitialContentBundleEntryMetaDataCollector {
private static final double THRESHOLD_RATIO = 10;
private static final int BUFFER = 512;
private static final long TOOBIG = 0x6400000; // Max size of unzipped data, 100MB
private static final String ZIP_ENTRY_SEPARATOR = "/";
private final BundleSlingInitialContentExtractContext context;
private final String basePath;
private final ContentPackage2FeatureModelConverter contentPackage2FeatureModelConverter;
private final Path newBundleFile;
private final Set<SlingInitialContentBundleEntryMetaData> collectedSlingInitialContentBundleEntries = new HashSet<>();
private final AtomicLong total = new AtomicLong(0);
private final JarFile jarFile;
SlingInitialContentBundleEntryMetaDataCollector(@NotNull BundleSlingInitialContentExtractContext context,
@NotNull ContentPackage2FeatureModelConverter contentPackage2FeatureModelConverter,
@NotNull Path newBundleFile) {
this.context = context;
this.basePath = contentPackage2FeatureModelConverter.getTempDirectory().getPath();
this.contentPackage2FeatureModelConverter = contentPackage2FeatureModelConverter;
this.newBundleFile = newBundleFile;
this.jarFile = context.getJarFile();
}
/**
* Collects all the MetaData from the context into a set
*
* @return
* @throws IOException
*/
@SuppressWarnings("java:S5042") // we already addressed this
@NotNull
Set<SlingInitialContentBundleEntryMetaData> collectFromContextAndWriteTmpFiles() throws IOException {
final Manifest manifest = context.getManifest();
// create JAR file to prevent extracting it twice and for random access
try (OutputStream fileOutput = Files.newOutputStream(newBundleFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
JarOutputStream bundleOutput = new JarOutputStream(fileOutput, manifest)) {
Enumeration<? extends JarEntry> entries = jarFile.entries();
// first we collect all the entries into a set, collectedSlingInitialContentBundleEntries.
// we need it up front to be perform various checks in another loop later.
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
if (jarEntry.getName().equals(JarFile.MANIFEST_NAME)) {
continue;
}
if (!jarEntry.isDirectory()) {
extractFile(jarEntry, bundleOutput);
}
if (total.get() + BUFFER > TOOBIG) {
throw new IllegalStateException("Sling-Initial-Content: File content being unzipped is too big "
+ "(>" + FileUtils.byteCountToDisplaySize(TOOBIG) + "): " + context.getPath());
}
}
}
return collectedSlingInitialContentBundleEntries;
}
private void extractFile(JarEntry jarEntry, JarOutputStream bundleOutput) throws IOException {
byte[] data = new byte[BUFFER];
long compressedSize = jarEntry.getCompressedSize();
try (InputStream input = new BufferedInputStream(jarFile.getInputStream(jarEntry))) {
if (jarEntryIsSlingInitialContent(context, jarEntry)) {
File targetFile = new File(contentPackage2FeatureModelConverter.getTempDirectory(), jarEntry.getName().replace('/', File.separatorChar));
String canonicalDestinationPath = targetFile.getCanonicalPath();
if (!checkIfPathStartsWithOrIsEqual(contentPackage2FeatureModelConverter.getTempDirectory().getCanonicalPath(), canonicalDestinationPath, File.separator)) {
throw new IOException("Entry is outside of the target directory " + canonicalDestinationPath);
}
targetFile.getParentFile().mkdirs();
if (!targetFile.exists() && !targetFile.createNewFile()) {
throw new IOException("Could not create placeholder file!");
}
FileOutputStream fos = new FileOutputStream(targetFile);
safelyWriteOutputStream(compressedSize, data, input, fos, true);
SlingInitialContentBundleEntryMetaData bundleEntry = createSlingInitialContentBundleEntry(context, targetFile);
collectedSlingInitialContentBundleEntries.add(bundleEntry);
} else {
//write 'normal' content out to the normal bundle output
bundleOutput.putNextEntry(jarEntry);
safelyWriteOutputStream(compressedSize, data, input, bundleOutput, false);
IOUtils.copy(input, bundleOutput);
bundleOutput.closeEntry();
}
}
}
private void safelyWriteOutputStream(long compressedSize,
byte[] data,
@NotNull InputStream input,
@NotNull OutputStream fos,
boolean shouldClose) throws IOException {
int count;
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
while (total.get() + BUFFER <= TOOBIG && (count = input.read(data, 0, BUFFER)) != -1) {
dest.write(data, 0, count);
total.addAndGet(count);
double compressionRatio = (double) count / compressedSize;
if (compressionRatio > THRESHOLD_RATIO) {
// ratio between compressed and uncompressed data is highly suspicious, looks like a Zip Bomb Attack
break;
}
}
dest.flush();
if (shouldClose) {
dest.close();
}
}
private boolean jarEntryIsSlingInitialContent(@NotNull BundleSlingInitialContentExtractContext context, @NotNull JarEntry jarEntry) {
final String entryName = jarEntry.getName();
return context.getPathEntryList().stream().anyMatch(
pathEntry -> checkIfPathStartsWithOrIsEqual(pathEntry.getPath(), entryName, ZIP_ENTRY_SEPARATOR)
);
}
@NotNull
private SlingInitialContentBundleEntryMetaData createSlingInitialContentBundleEntry(@NotNull BundleSlingInitialContentExtractContext context,
@NotNull File sourceFile) throws UnsupportedEncodingException {
final String entryName = StringUtils.replace(StringUtils.substringAfter(sourceFile.getPath(), basePath + File.separator), File.separator, ZIP_ENTRY_SEPARATOR);
final PathEntry pathEntryValue = context.getPathEntryList().stream().filter(
pathEntry -> checkIfPathStartsWithOrIsEqual(pathEntry.getPath(), entryName, ZIP_ENTRY_SEPARATOR)
).findFirst().orElseThrow(NullPointerException::new);
final String target = pathEntryValue.getTarget();
// https://sling.apache.org/documentation/bundles/content-loading-jcr-contentloader.html#file-name-escaping
String repositoryPath = (target != null ? target : "/") + URLDecoder.decode(entryName.substring(pathEntryValue.getPath().length()), "UTF-8");
return new SlingInitialContentBundleEntryMetaData(sourceFile, pathEntryValue, repositoryPath);
}
private static boolean checkIfPathStartsWithOrIsEqual(String pathA, String pathB, String fileSeparator) {
String fixedPath = pathA;
if (!fixedPath.endsWith(fileSeparator)) {
fixedPath = pathA + fileSeparator;
}
return pathB.startsWith(fixedPath) || pathB.equals(pathA);
}
}