mirror of
				https://github.com/apache/cloudstack.git
				synced 2025-10-26 08:42:29 +01:00 
			
		
		
		
	Validate QCOW2 on upload and register
This commit is contained in:
		
							parent
							
								
									b97bd3bee1
								
							
						
					
					
						commit
						1861e78361
					
				| @ -0,0 +1,51 @@ | |||||||
|  | // 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.cloudstack.storage.formatinspector; | ||||||
|  | 
 | ||||||
|  | public enum Qcow2HeaderField { | ||||||
|  |     MAGIC(0, 4), | ||||||
|  |     VERSION(4, 4), | ||||||
|  |     BACKING_FILE_OFFSET(8, 8), | ||||||
|  |     BACKING_FILE_NAME_LENGTH(16, 4), | ||||||
|  |     CLUSTER_BITS(20, 4), | ||||||
|  |     SIZE(24, 8), | ||||||
|  |     CRYPT_METHOD(32, 4), | ||||||
|  |     L1_SIZE(36, 4), | ||||||
|  |     LI_TABLE_OFFSET(40, 8), | ||||||
|  |     REFCOUNT_TABLE_OFFSET(48, 8), | ||||||
|  |     REFCOUNT_TABLE_CLUSTERS(56, 4), | ||||||
|  |     NB_SNAPSHOTS(60, 4), | ||||||
|  |     SNAPSHOTS_OFFSET(64, 8), | ||||||
|  |     INCOMPATIBLE_FEATURES(72, 8); | ||||||
|  | 
 | ||||||
|  |     private final int offset; | ||||||
|  |     private final int length; | ||||||
|  | 
 | ||||||
|  |     Qcow2HeaderField(int offset, int length) { | ||||||
|  |         this.offset = offset; | ||||||
|  |         this.length = length; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getLength() { | ||||||
|  |         return length; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getOffset() { | ||||||
|  |         return offset; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,267 @@ | |||||||
|  | // 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.cloudstack.storage.formatinspector; | ||||||
|  | 
 | ||||||
|  | import com.cloud.utils.NumbersUtil; | ||||||
|  | import org.apache.commons.lang3.ArrayUtils; | ||||||
|  | import org.apache.log4j.Logger; | ||||||
|  | 
 | ||||||
|  | import java.io.FileInputStream; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class to inspect QCOW2 files/objects. In our context, a QCOW2 might be a threat to the environment if it meets one of the following criteria when coming from external sources | ||||||
|  |  * (like registering or uploading volumes and templates): | ||||||
|  |  * <ul> | ||||||
|  |  *   <li>has a backing file reference;</li> | ||||||
|  |  *   <li>has an external data file reference;</li> | ||||||
|  |  *   <li>has unknown incompatible features.</li> | ||||||
|  |  * </ul> | ||||||
|  |  * | ||||||
|  |  * The implementation was done based on the <a href="https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qcow2.txt"> QEMU's official interoperability documentation</a> | ||||||
|  |  * and on the <a href="https://review.opendev.org/c/openstack/cinder/+/923247/2/cinder/image/format_inspector.py">OpenStack's Cinder implementation for Python</a>. | ||||||
|  |  */ | ||||||
|  | public class Qcow2Inspector { | ||||||
|  |     protected static Logger LOGGER = Logger.getLogger(Qcow2Inspector.class); | ||||||
|  | 
 | ||||||
|  |     private static final byte[] QCOW_MAGIC_STRING = ArrayUtils.add("QFI".getBytes(), (byte) 0xfb); | ||||||
|  |     private static final int INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT = 4; | ||||||
|  |     private static final int INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE = 0; | ||||||
|  |     private static final int EXTERNAL_DATA_FILE_BYTE_POSITION = 7; | ||||||
|  |     private static final int EXTERNAL_DATA_FILE_BIT = 2; | ||||||
|  |     private static final byte EXTERNAL_DATA_FILE_BITMASK = (byte) (1 << EXTERNAL_DATA_FILE_BIT); | ||||||
|  | 
 | ||||||
|  |     private static final Set<Qcow2HeaderField> SET_OF_HEADER_FIELDS_TO_READ = Set.of(Qcow2HeaderField.MAGIC, | ||||||
|  |             Qcow2HeaderField.VERSION, | ||||||
|  |             Qcow2HeaderField.SIZE, | ||||||
|  |             Qcow2HeaderField.BACKING_FILE_OFFSET, | ||||||
|  |             Qcow2HeaderField.INCOMPATIBLE_FEATURES); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Validates if the file is a valid and allowed QCOW2 (i.e.: does not contain external references). | ||||||
|  |      * @param filePath Path of the file to be validated. | ||||||
|  |      * @throws RuntimeException If the QCOW2 file meets one of the following criteria: | ||||||
|  |      * <ul> | ||||||
|  |      *   <li>has a backing file reference;</li> | ||||||
|  |      *   <li>has an external data file reference;</li> | ||||||
|  |      *   <li>has unknown incompatible features.</li> | ||||||
|  |      * </ul> | ||||||
|  |      */ | ||||||
|  |     public static void validateQcow2File(String filePath) throws RuntimeException { | ||||||
|  |         LOGGER.info(String.format("Verifying if [%s] is a valid and allowed QCOW2 file .", filePath)); | ||||||
|  | 
 | ||||||
|  |         Map<String, byte[]> headerFieldsAndValues; | ||||||
|  |         try (InputStream inputStream = new FileInputStream(filePath)) { | ||||||
|  |             headerFieldsAndValues = unravelQcow2Header(inputStream, filePath); | ||||||
|  |         } catch (IOException ex) { | ||||||
|  |             throw new RuntimeException(String.format("Unable to validate file [%s] due to: ", filePath), ex); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         validateQcow2HeaderFields(headerFieldsAndValues, filePath); | ||||||
|  | 
 | ||||||
|  |         LOGGER.info(String.format("[%s] is a valid and allowed QCOW2 file.", filePath)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Unravels the QCOW2 header in a serial fashion, iterating through the {@link Qcow2HeaderField}, reading the fields specified in | ||||||
|  |      * {@link Qcow2Inspector#SET_OF_HEADER_FIELDS_TO_READ} and skipping the others. | ||||||
|  |      * @param qcow2InputStream InputStream of the QCOW2 being unraveled. | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @return A map of the header fields and their values according to the {@link Qcow2Inspector#SET_OF_HEADER_FIELDS_TO_READ}. | ||||||
|  |      * @throws IOException If the field cannot be read or skipped. | ||||||
|  |      */ | ||||||
|  |     public static Map<String, byte[]> unravelQcow2Header(InputStream qcow2InputStream, String qcow2LogReference) throws IOException { | ||||||
|  |         Map<String, byte[]> result = new HashMap<>(); | ||||||
|  | 
 | ||||||
|  |         LOGGER.debug(String.format("Unraveling QCOW2 [%s] headers.", qcow2LogReference)); | ||||||
|  |         for (Qcow2HeaderField qcow2Header : Qcow2HeaderField.values()) { | ||||||
|  |             if (!SET_OF_HEADER_FIELDS_TO_READ.contains(qcow2Header)) { | ||||||
|  |                 skipHeader(qcow2InputStream, qcow2Header, qcow2LogReference); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             byte[] headerValue = readHeader(qcow2InputStream, qcow2Header, qcow2LogReference); | ||||||
|  |             result.put(qcow2Header.name(), headerValue); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Skips the field's length in the InputStream. | ||||||
|  |      * @param qcow2InputStream InputStream of the QCOW2 being unraveled. | ||||||
|  |      * @param field Field being skipped (name and length). | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws IOException If the bytes skipped do not match the field length. | ||||||
|  |      */ | ||||||
|  |     protected static void skipHeader(InputStream qcow2InputStream, Qcow2HeaderField field, String qcow2LogReference) throws IOException { | ||||||
|  |         LOGGER.trace(String.format("Skipping field [%s] of QCOW2 [%s].", field, qcow2LogReference)); | ||||||
|  | 
 | ||||||
|  |         if (qcow2InputStream.skip(field.getLength()) != field.getLength()) { | ||||||
|  |             throw new IOException(String.format("Unable to skip field [%s] of QCOW2 [%s].", field, qcow2LogReference)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reads the field's length in the InputStream. | ||||||
|  |      * @param qcow2InputStream InputStream of the QCOW2 being unraveled. | ||||||
|  |      * @param field Field being read (name and length). | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws IOException If the bytes read do not match the field length. | ||||||
|  |      */ | ||||||
|  |     protected static byte[] readHeader(InputStream qcow2InputStream, Qcow2HeaderField field, String qcow2LogReference) throws IOException { | ||||||
|  |         byte[] readBytes = new byte[field.getLength()]; | ||||||
|  | 
 | ||||||
|  |         LOGGER.trace(String.format("Reading field [%s] of QCOW2 [%s].", field, qcow2LogReference)); | ||||||
|  |         if (qcow2InputStream.read(readBytes) != field.getLength()) { | ||||||
|  |             throw new IOException(String.format("Unable to read field [%s] of QCOW2 [%s].", field, qcow2LogReference)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         LOGGER.trace(String.format("Read %s as field [%s] of QCOW2 [%s].", ArrayUtils.toString(readBytes), field, qcow2LogReference)); | ||||||
|  |         return readBytes; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Validates the values of the header fields {@link Qcow2HeaderField#MAGIC}, {@link Qcow2HeaderField#BACKING_FILE_OFFSET}, and {@link Qcow2HeaderField#INCOMPATIBLE_FEATURES}. | ||||||
|  |      * @param headerFieldsAndValues A map of the header fields and their values. | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws SecurityException If the QCOW2 does not contain the QCOW magic string or contains a backing file reference or incompatible features. | ||||||
|  |      */ | ||||||
|  |     public static void validateQcow2HeaderFields(Map<String, byte[]> headerFieldsAndValues, String qcow2LogReference) throws SecurityException{ | ||||||
|  |         byte[] fieldValue = headerFieldsAndValues.get(Qcow2HeaderField.MAGIC.name()); | ||||||
|  |         validateQcowMagicString(fieldValue, qcow2LogReference); | ||||||
|  | 
 | ||||||
|  |         fieldValue = headerFieldsAndValues.get(Qcow2HeaderField.BACKING_FILE_OFFSET.name()); | ||||||
|  |         validateAbsenceOfBackingFileReference(NumbersUtil.bytesToLong(fieldValue), qcow2LogReference); | ||||||
|  | 
 | ||||||
|  |         fieldValue = headerFieldsAndValues.get(Qcow2HeaderField.INCOMPATIBLE_FEATURES.name()); | ||||||
|  |         validateAbsenceOfIncompatibleFeatures(fieldValue, qcow2LogReference); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verifies if the first 4 bytes of the header are the QCOW magic string. Throws an exception if not. | ||||||
|  |      * @param headerMagicString The first 4 bytes of the header. | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws SecurityException If the header's magic string is not the QCOW magic string. | ||||||
|  |      */ | ||||||
|  |     private static void validateQcowMagicString(byte[] headerMagicString, String qcow2LogReference) throws SecurityException { | ||||||
|  |         LOGGER.debug(String.format("Verifying if [%s] has a valid QCOW magic string.", qcow2LogReference)); | ||||||
|  | 
 | ||||||
|  |         if (!Arrays.equals(QCOW_MAGIC_STRING, headerMagicString)) { | ||||||
|  |             throw new SecurityException(String.format("[%s] is not a valid QCOW2 because its first 4 bytes are not the QCOW magic string.", qcow2LogReference)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         LOGGER.debug(String.format("[%s] has a valid QCOW magic string.", qcow2LogReference)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verifies if the QCOW2 has a backing file and throws an exception if so. | ||||||
|  |      * @param backingFileOffset The backing file offset value of the QCOW2 header. | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws SecurityException If the QCOW2 has a backing file reference. | ||||||
|  |      */ | ||||||
|  |     private static void validateAbsenceOfBackingFileReference(long backingFileOffset, String qcow2LogReference) throws SecurityException { | ||||||
|  |         LOGGER.debug(String.format("Verifying if [%s] has a backing file reference.", qcow2LogReference)); | ||||||
|  | 
 | ||||||
|  |         if (backingFileOffset != 0) { | ||||||
|  |             throw new SecurityException(String.format("[%s] has a backing file reference. This can be an attack to the infrastructure; therefore, we will not accept" + | ||||||
|  |                     " this QCOW2.", qcow2LogReference)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         LOGGER.debug(String.format("[%s] does not have a backing file reference.", qcow2LogReference)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verifies if the QCOW2 has incompatible features and throw an exception if it has an external data file reference or unknown incompatible features. | ||||||
|  |      * @param incompatibleFeatures The incompatible features bytes of the QCOW2 header. | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws SecurityException If the QCOW2 has an external data file reference or unknown incompatible features. | ||||||
|  |      */ | ||||||
|  |     private static void validateAbsenceOfIncompatibleFeatures(byte[] incompatibleFeatures, String qcow2LogReference) throws SecurityException { | ||||||
|  |         LOGGER.debug(String.format("Verifying if [%s] has incompatible features.", qcow2LogReference)); | ||||||
|  | 
 | ||||||
|  |         if (NumbersUtil.bytesToLong(incompatibleFeatures) == 0) { | ||||||
|  |             LOGGER.debug(String.format("[%s] does not have incompatible features.", qcow2LogReference)); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         LOGGER.debug(String.format("[%s] has incompatible features.", qcow2LogReference)); | ||||||
|  | 
 | ||||||
|  |         validateAbsenceOfExternalDataFileReference(incompatibleFeatures, qcow2LogReference); | ||||||
|  |         validateAbsenceOfUnknownIncompatibleFeatures(incompatibleFeatures, qcow2LogReference); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verifies if the QCOW2 has an external data file reference and throw an exception if so. | ||||||
|  |      * @param incompatibleFeatures The incompatible features bytes of the QCOW2 header. | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws SecurityException If the QCOW2 has an external data file reference. | ||||||
|  |      */ | ||||||
|  |     private static void validateAbsenceOfExternalDataFileReference(byte[] incompatibleFeatures, String qcow2LogReference) throws SecurityException { | ||||||
|  |         LOGGER.debug(String.format("Verifying if [%s] has an external data file reference.", qcow2LogReference)); | ||||||
|  | 
 | ||||||
|  |         if ((incompatibleFeatures[EXTERNAL_DATA_FILE_BYTE_POSITION] & EXTERNAL_DATA_FILE_BITMASK) != 0) { | ||||||
|  |             throw new SecurityException(String.format("[%s] has an external data file reference. This can be an attack to the infrastructure; therefore, we will discard" + | ||||||
|  |                     " this file.", qcow2LogReference)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         LOGGER.info(String.format("[%s] does not have an external data file reference.", qcow2LogReference)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verifies if the QCOW2 has unknown incompatible features and throw an exception if so. | ||||||
|  |      * <br/><br/> | ||||||
|  |      * Unknown incompatible features are those with bit greater than | ||||||
|  |      * {@link Qcow2Inspector#INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT}, which will be the represented by bytes in positions greater than | ||||||
|  |      * {@link Qcow2Inspector#INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE} (in Big Endian order). Therefore, we expect that those bytes are always zero. If not, an exception is thrown. | ||||||
|  |      * @param incompatibleFeatures The incompatible features bytes of the QCOW2 header. | ||||||
|  |      * @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions. | ||||||
|  |      * @throws SecurityException If the QCOW2 has unknown incompatible features. | ||||||
|  |      */ | ||||||
|  |     private static void validateAbsenceOfUnknownIncompatibleFeatures(byte[] incompatibleFeatures, String qcow2LogReference) throws SecurityException { | ||||||
|  |         LOGGER.debug(String.format("Verifying if [%s] has unknown incompatible features [%s].", qcow2LogReference, ArrayUtils.toString(incompatibleFeatures))); | ||||||
|  | 
 | ||||||
|  |         for (int byteNum = incompatibleFeatures.length - 1; byteNum >= 0; byteNum--) { | ||||||
|  |             int bytePosition = incompatibleFeatures.length - 1 - byteNum; | ||||||
|  |             LOGGER.trace(String.format("Looking for unknown incompatible feature bit in position [%s].", bytePosition)); | ||||||
|  | 
 | ||||||
|  |             byte bitmask = 0; | ||||||
|  |             if (byteNum == INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE) { | ||||||
|  |                 bitmask = ((1 << INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT) - 1); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             LOGGER.trace(String.format("Bitmask for byte in position [%s] is [%s].", bytePosition, Integer.toBinaryString(bitmask))); | ||||||
|  | 
 | ||||||
|  |             int featureBit = incompatibleFeatures[bytePosition] & ~bitmask; | ||||||
|  |             if (featureBit != 0) { | ||||||
|  |                 throw new SecurityException(String.format("Found unknown incompatible feature bit [%s] in byte [%s] of [%s]. This can be an attack to the infrastructure; " + | ||||||
|  |                         "therefore, we will discard this QCOW2.", featureBit, bytePosition + Qcow2HeaderField.INCOMPATIBLE_FEATURES.getOffset(), qcow2LogReference)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             LOGGER.trace(String.format("Did not find unknown incompatible feature in position [%s].", bytePosition)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         LOGGER.info(String.format("[%s] does not have unknown incompatible features.", qcow2LogReference)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -66,6 +66,7 @@ import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; | |||||||
| import org.apache.cloudstack.storage.command.UploadStatusCommand; | import org.apache.cloudstack.storage.command.UploadStatusCommand; | ||||||
| import org.apache.cloudstack.storage.configdrive.ConfigDrive; | import org.apache.cloudstack.storage.configdrive.ConfigDrive; | ||||||
| import org.apache.cloudstack.storage.configdrive.ConfigDriveBuilder; | import org.apache.cloudstack.storage.configdrive.ConfigDriveBuilder; | ||||||
|  | import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector; | ||||||
| import org.apache.cloudstack.storage.template.DownloadManager; | import org.apache.cloudstack.storage.template.DownloadManager; | ||||||
| import org.apache.cloudstack.storage.template.DownloadManagerImpl; | import org.apache.cloudstack.storage.template.DownloadManagerImpl; | ||||||
| import org.apache.cloudstack.storage.template.UploadEntity; | import org.apache.cloudstack.storage.template.UploadEntity; | ||||||
| @ -3446,8 +3447,19 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S | |||||||
|             return result; |             return result; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         String finalFilename = resourcePath + "/" + templateFilename; | ||||||
|  | 
 | ||||||
|  |         if (ImageStoreUtil.isCorrectExtension(finalFilename, "qcow2")) { | ||||||
|  |             try { | ||||||
|  |                 Qcow2Inspector.validateQcow2File(finalFilename); | ||||||
|  |             } catch (RuntimeException e) { | ||||||
|  |                 s_logger.error(String.format("Uploaded file [%s] is not a valid QCOW2.", finalFilename), e); | ||||||
|  |                 return "The uploaded file is not a valid QCOW2. Ask the administrator to check the logs for more details."; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // Set permissions for the downloaded template |         // Set permissions for the downloaded template | ||||||
|         File downloadedTemplate = new File(resourcePath + "/" + templateFilename); |         File downloadedTemplate = new File(finalFilename); | ||||||
|         _storage.setWorldReadableAndWriteable(downloadedTemplate); |         _storage.setWorldReadableAndWriteable(downloadedTemplate); | ||||||
| 
 | 
 | ||||||
|         // Set permissions for template/volume.properties |         // Set permissions for template/volume.properties | ||||||
|  | |||||||
| @ -62,6 +62,11 @@ import org.apache.cloudstack.storage.command.DownloadProgressCommand.RequestType | |||||||
| import org.apache.cloudstack.storage.NfsMountManagerImpl.PathParser; | import org.apache.cloudstack.storage.NfsMountManagerImpl.PathParser; | ||||||
| import org.apache.cloudstack.storage.resource.NfsSecondaryStorageResource; | import org.apache.cloudstack.storage.resource.NfsSecondaryStorageResource; | ||||||
| import org.apache.cloudstack.storage.resource.SecondaryStorageResource; | import org.apache.cloudstack.storage.resource.SecondaryStorageResource; | ||||||
|  | import org.apache.cloudstack.storage.formatinspector.Qcow2HeaderField; | ||||||
|  | import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector; | ||||||
|  | import org.apache.cloudstack.utils.security.ChecksumValue; | ||||||
|  | import org.apache.cloudstack.utils.security.DigestHelper; | ||||||
|  | 
 | ||||||
| import org.apache.log4j.Logger; | import org.apache.log4j.Logger; | ||||||
| 
 | 
 | ||||||
| import com.cloud.agent.api.storage.DownloadAnswer; | import com.cloud.agent.api.storage.DownloadAnswer; | ||||||
| @ -80,10 +85,7 @@ import com.cloud.utils.NumbersUtil; | |||||||
| import com.cloud.utils.component.ManagerBase; | import com.cloud.utils.component.ManagerBase; | ||||||
| import com.cloud.utils.exception.CloudRuntimeException; | import com.cloud.utils.exception.CloudRuntimeException; | ||||||
| import com.cloud.utils.script.Script; | import com.cloud.utils.script.Script; | ||||||
| import com.cloud.utils.storage.QCOW2Utils; | import com.cloud.utils.StringUtils; | ||||||
| import org.apache.cloudstack.utils.security.ChecksumValue; |  | ||||||
| import org.apache.cloudstack.utils.security.DigestHelper; |  | ||||||
| import org.apache.commons.lang3.StringUtils; |  | ||||||
| 
 | 
 | ||||||
| import static com.cloud.utils.NumbersUtil.toHumanReadableSize; | import static com.cloud.utils.NumbersUtil.toHumanReadableSize; | ||||||
| 
 | 
 | ||||||
| @ -346,11 +348,17 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager | |||||||
|             // The QCOW2 is the only format with a header, |             // The QCOW2 is the only format with a header, | ||||||
|             // and as such can be easily read. |             // and as such can be easily read. | ||||||
| 
 | 
 | ||||||
|             try (InputStream inputStream = td.getS3ObjectInputStream();) { |             try (InputStream inputStream = td.getS3ObjectInputStream()) { | ||||||
|                 dnld.setTemplatesize(QCOW2Utils.getVirtualSize(inputStream, false)); |                 Map<String, byte[]> qcow2HeaderFieldsAndValues = Qcow2Inspector.unravelQcow2Header(inputStream, td.getDownloadUrl()); | ||||||
|             } |                 Qcow2Inspector.validateQcow2HeaderFields(qcow2HeaderFieldsAndValues, td.getDownloadUrl()); | ||||||
|             catch (IOException e) { | 
 | ||||||
|                 result = "Couldn't read QCOW2 virtual size. Error: " + e.getMessage(); |                 dnld.setTemplatesize(NumbersUtil.bytesToLong(qcow2HeaderFieldsAndValues.get(Qcow2HeaderField.SIZE.name()))); | ||||||
|  |             } catch (IOException ex) { | ||||||
|  |                 result = String.format("Unable to read QCOW2 metadata. Error: %s", ex.getMessage()); | ||||||
|  |                 LOGGER.error(result, ex); | ||||||
|  |             } catch (SecurityException ex) { | ||||||
|  |                 result = String.format("[%s] is not a valid QCOW2:", td.getDownloadUrl()); | ||||||
|  |                 LOGGER.error(result, ex); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
| @ -405,8 +413,19 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager | |||||||
|             return result; |             return result; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         String finalFilename = resourcePath + "/" + templateFilename; | ||||||
|  | 
 | ||||||
|  |         if (ImageFormat.QCOW2.equals(dnld.getFormat())) { | ||||||
|  |             try { | ||||||
|  |                 Qcow2Inspector.validateQcow2File(finalFilename); | ||||||
|  |             } catch (RuntimeException e) { | ||||||
|  |                 LOGGER.error(String.format("The downloaded file [%s] is not a valid QCOW2.", finalFilename), e); | ||||||
|  |                 return "The downloaded file is not a valid QCOW2. Ask the administrator to check the logs for more details."; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // Set permissions for the downloaded template |         // Set permissions for the downloaded template | ||||||
|         File downloadedTemplate = new File(resourcePath + "/" + templateFilename); |         File downloadedTemplate = new File(finalFilename); | ||||||
| 
 | 
 | ||||||
|         _storage.setWorldReadableAndWriteable(downloadedTemplate); |         _storage.setWorldReadableAndWriteable(downloadedTemplate); | ||||||
|         setPermissionsForTheDownloadedTemplate(dnld, resourcePath, resourceType); |         setPermissionsForTheDownloadedTemplate(dnld, resourcePath, resourceType); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user