Implement 'Signed Large CSAR' support
[sdc.git] / common-be / src / main / java / org / openecomp / sdc / be / csar / storage / CsarSizeReducer.java
1 /*
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2021 Nordix Foundation
4  *  ================================================================================
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20
21 package org.openecomp.sdc.be.csar.storage;
22
23 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
24
25 import java.io.BufferedOutputStream;
26 import java.io.IOException;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.UUID;
32 import java.util.concurrent.atomic.AtomicBoolean;
33 import java.util.function.Consumer;
34 import java.util.stream.Collectors;
35 import java.util.zip.ZipEntry;
36 import java.util.zip.ZipFile;
37 import java.util.zip.ZipOutputStream;
38 import lombok.Getter;
39 import org.apache.commons.collections4.CollectionUtils;
40 import org.apache.commons.io.FilenameUtils;
41 import org.openecomp.sdc.be.csar.storage.exception.CsarSizeReducerException;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 public class CsarSizeReducer implements PackageSizeReducer {
46
47     private static final Logger LOGGER = LoggerFactory.getLogger(CsarSizeReducer.class);
48     private static final Set<String> ALLOWED_SIGNATURE_EXTENSIONS = Set.of("cms");
49     private static final Set<String> ALLOWED_CERTIFICATE_EXTENSIONS = Set.of("cert", "crt");
50     private static final String CSAR_EXTENSION = "csar";
51     private static final String UNEXPECTED_PROBLEM_HAPPENED_WHILE_READING_THE_CSAR = "An unexpected problem happened while reading the CSAR '%s'";
52     @Getter
53     private final AtomicBoolean reduced = new AtomicBoolean(false);
54
55     private final CsarPackageReducerConfiguration configuration;
56
57     public CsarSizeReducer(final CsarPackageReducerConfiguration configuration) {
58         this.configuration = configuration;
59     }
60
61     @Override
62     public byte[] reduce(final Path csarPackagePath) {
63         if (hasSignedPackageStructure(csarPackagePath)) {
64             return reduce(csarPackagePath, this::signedZipProcessingConsumer);
65         } else {
66             return reduce(csarPackagePath, this::unsignedZipProcessingConsumer);
67         }
68     }
69
70     private byte[] reduce(final Path csarPackagePath, final ZipProcessFunction zipProcessingFunction) {
71         final var reducedCsarPath = Path.of(csarPackagePath + "." + UUID.randomUUID());
72
73         try (final var zf = new ZipFile(csarPackagePath.toString());
74             final var zos = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(reducedCsarPath)))) {
75             zf.entries().asIterator().forEachRemaining(zipProcessingFunction.getProcessZipConsumer(csarPackagePath, zf, zos));
76         } catch (final IOException ex1) {
77             rollback(reducedCsarPath);
78             final var errorMsg = String.format(UNEXPECTED_PROBLEM_HAPPENED_WHILE_READING_THE_CSAR, csarPackagePath);
79             throw new CsarSizeReducerException(errorMsg, ex1);
80         }
81         final byte[] reducedCsarBytes;
82         try {
83             if (reduced.get()) {
84                 reducedCsarBytes = Files.readAllBytes(reducedCsarPath);
85             } else {
86                 reducedCsarBytes = Files.readAllBytes(csarPackagePath);
87             }
88         } catch (final IOException e) {
89             final var errorMsg = String.format("Could not read bytes of file '%s'", csarPackagePath);
90             throw new CsarSizeReducerException(errorMsg, e);
91         }
92         try {
93             Files.delete(reducedCsarPath);
94         } catch (final IOException e) {
95             final var errorMsg = String.format("Could not delete temporary file '%s'", reducedCsarPath);
96             throw new CsarSizeReducerException(errorMsg, e);
97         }
98
99         return reducedCsarBytes;
100     }
101
102     private Consumer<ZipEntry> signedZipProcessingConsumer(final Path csarPackagePath, final ZipFile zf, final ZipOutputStream zos) {
103         return zipEntry -> {
104             final var entryName = zipEntry.getName();
105             try {
106                 zos.putNextEntry(new ZipEntry(entryName));
107                 if (!zipEntry.isDirectory()) {
108                     if (entryName.toLowerCase().endsWith(CSAR_EXTENSION)) {
109                         final var internalCsarExtractPath = Path.of(csarPackagePath + "." + UUID.randomUUID());
110                         Files.copy(zf.getInputStream(zipEntry), internalCsarExtractPath, REPLACE_EXISTING);
111                         zos.write(reduce(internalCsarExtractPath, this::unsignedZipProcessingConsumer));
112                         Files.delete(internalCsarExtractPath);
113                     } else {
114                         zos.write(zf.getInputStream(zipEntry).readAllBytes());
115                     }
116                 }
117                 zos.closeEntry();
118             } catch (final IOException ei) {
119                 final var errorMsg = String.format("Failed to extract '%s' from zip '%s'", entryName, csarPackagePath);
120                 throw new CsarSizeReducerException(errorMsg, ei);
121             }
122         };
123     }
124
125     private Consumer<ZipEntry> unsignedZipProcessingConsumer(final Path csarPackagePath, final ZipFile zf, final ZipOutputStream zos) {
126         return zipEntry -> {
127             final var entryName = zipEntry.getName();
128             try {
129                 zos.putNextEntry(new ZipEntry(entryName));
130                 if (!zipEntry.isDirectory()) {
131                     if (isCandidateToRemove(zipEntry)) {
132                         // replace with EMPTY string to avoid package description inconsistency/validation errors
133                         zos.write("".getBytes());
134                         reduced.set(true);
135                     } else {
136                         zos.write(zf.getInputStream(zipEntry).readAllBytes());
137                     }
138                 }
139                 zos.closeEntry();
140             } catch (final IOException ei) {
141                 final var errorMsg = String.format("Failed to extract '%s' from zip '%s'", entryName, csarPackagePath);
142                 throw new CsarSizeReducerException(errorMsg, ei);
143             }
144         };
145     }
146
147     private void rollback(final Path reducedCsarPath) {
148         if (Files.exists(reducedCsarPath)) {
149             try {
150                 Files.delete(reducedCsarPath);
151             } catch (final Exception ex2) {
152                 LOGGER.warn("Could not delete temporary file '{}'", reducedCsarPath, ex2);
153             }
154         }
155     }
156
157     private boolean isCandidateToRemove(final ZipEntry zipEntry) {
158         final String zipEntryName = zipEntry.getName();
159         return configuration.getFoldersToStrip().stream().anyMatch(Path.of(zipEntryName)::startsWith)
160             || zipEntry.getSize() > configuration.getSizeLimit();
161     }
162
163     private boolean hasSignedPackageStructure(final Path csarPackagePath) {
164         final List<Path> packagePathList;
165         try (final var zf = new ZipFile(csarPackagePath.toString())) {
166             packagePathList = zf.stream()
167                 .filter(zipEntry -> !zipEntry.isDirectory())
168                 .map(ZipEntry::getName).map(Path::of)
169                 .collect(Collectors.toList());
170         } catch (final IOException e) {
171             final var errorMsg = String.format(UNEXPECTED_PROBLEM_HAPPENED_WHILE_READING_THE_CSAR, csarPackagePath);
172             throw new CsarSizeReducerException(errorMsg, e);
173         }
174
175         if (CollectionUtils.isEmpty(packagePathList)) {
176             return false;
177         }
178         final int numberOfFiles = packagePathList.size();
179         if (numberOfFiles == 2) {
180             return hasOneInternalPackageFile(packagePathList) && hasOneSignatureFile(packagePathList);
181         }
182         if (numberOfFiles == 3) {
183             return hasOneInternalPackageFile(packagePathList) && hasOneSignatureFile(packagePathList) && hasOneCertificateFile(packagePathList);
184         }
185         return false;
186     }
187
188     private boolean hasOneInternalPackageFile(final List<Path> packagePathList) {
189         return packagePathList.parallelStream()
190             .map(Path::toString)
191             .map(FilenameUtils::getExtension)
192             .map(String::toLowerCase)
193             .filter(extension -> extension.endsWith(CSAR_EXTENSION)).count() == 1;
194     }
195
196     private boolean hasOneSignatureFile(final List<Path> packagePathList) {
197         return packagePathList.parallelStream()
198             .map(Path::toString)
199             .map(FilenameUtils::getExtension)
200             .map(String::toLowerCase)
201             .filter(ALLOWED_SIGNATURE_EXTENSIONS::contains).count() == 1;
202     }
203
204     private boolean hasOneCertificateFile(final List<Path> packagePathList) {
205         return packagePathList.parallelStream()
206             .map(Path::toString)
207             .map(FilenameUtils::getExtension)
208             .map(String::toLowerCase)
209             .filter(ALLOWED_CERTIFICATE_EXTENSIONS::contains).count() == 1;
210     }
211
212     @FunctionalInterface
213     private interface ZipProcessFunction {
214
215         Consumer<ZipEntry> getProcessZipConsumer(Path csarPackagePath, ZipFile zf, ZipOutputStream zos);
216     }
217
218 }