| /* |
| * 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); |
| } |
| } |