001package org.apache.archiva.checksum; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import org.apache.archiva.common.utils.FileUtils; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026import java.io.FileNotFoundException; 027import java.io.IOException; 028import java.nio.charset.Charset; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.nio.file.StandardOpenOption; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.List; 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037 038import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE; 039import static org.apache.archiva.checksum.ChecksumValidationException.ValidationError.BAD_CHECKSUM_FILE_REF; 040 041/** 042 * ChecksummedFile 043 * <p>Terminology:</p> 044 * <dl> 045 * <dt>Checksum File</dt> 046 * <dd>The file that contains the previously calculated checksum value for the reference file. 047 * This is a text file with the extension ".sha1" or ".md5", and contains a single entry 048 * consisting of an optional reference filename, and a checksum string. 049 * </dd> 050 * <dt>Reference File</dt> 051 * <dd>The file that is being referenced in the checksum file.</dd> 052 * </dl> 053 */ 054public class ChecksummedFile 055{ 056 057 private static Charset FILE_ENCODING = Charset.forName( "UTF-8" ); 058 059 private final Logger log = LoggerFactory.getLogger( ChecksummedFile.class ); 060 061 private static final Pattern METADATA_PATTERN = Pattern.compile( "maven-metadata-\\S*.xml" ); 062 063 private final Path referenceFile; 064 065 /** 066 * Construct a ChecksummedFile object. 067 * 068 * @param referenceFile 069 */ 070 public ChecksummedFile( final Path referenceFile ) 071 { 072 this.referenceFile = referenceFile; 073 } 074 075 076 public static ChecksumReference getFromChecksumFile( Path checksumFile ) 077 { 078 ChecksumAlgorithm alg = ChecksumAlgorithm.getByExtension( checksumFile ); 079 ChecksummedFile file = new ChecksummedFile( getReferenceFile( checksumFile ) ); 080 return new ChecksumReference( file, alg, checksumFile ); 081 } 082 083 private static Path getReferenceFile( Path checksumFile ) 084 { 085 String fileName = checksumFile.getFileName( ).toString( ); 086 return checksumFile.resolveSibling( fileName.substring( 0, fileName.lastIndexOf( '.' ) ) ); 087 } 088 089 /** 090 * Calculate the checksum based on a given checksum. 091 * 092 * @param checksumAlgorithm the algorithm to use. 093 * @return the checksum string for the file. 094 * @throws IOException if unable to calculate the checksum. 095 */ 096 public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm ) 097 throws IOException 098 { 099 100 Checksum checksum = new Checksum( checksumAlgorithm ); 101 ChecksumUtil.update(checksum, referenceFile ); 102 return checksum.getChecksum( ); 103 } 104 105 /** 106 * Writes a checksum file for the referenceFile. 107 * 108 * @param checksumAlgorithm the hash to use. 109 * @return the checksum File that was created. 110 * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file. 111 */ 112 public Path writeFile(ChecksumAlgorithm checksumAlgorithm ) 113 throws IOException 114 { 115 Path checksumFile = referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getDefaultExtension() ); 116 Files.deleteIfExists( checksumFile ); 117 String checksum = calculateChecksum( checksumAlgorithm ); 118 Files.write( checksumFile, // 119 ( checksum + " " + referenceFile.getFileName( ).toString( ) ).getBytes( ), // 120 StandardOpenOption.CREATE_NEW ); 121 return checksumFile; 122 } 123 124 /** 125 * Get the checksum file for the reference file and hash. 126 * It returns a file for the given checksum, if one exists with one of the possible extensions. 127 * If it does not exist, a default path will be returned. 128 * 129 * @param checksumAlgorithm the hash that we are interested in. 130 * @return the checksum file to return 131 */ 132 public Path getChecksumFile( ChecksumAlgorithm checksumAlgorithm ) 133 { 134 for ( String ext : checksumAlgorithm.getExt( ) ) 135 { 136 Path file = referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getExt( ) ); 137 if ( Files.exists( file ) ) 138 { 139 return file; 140 } 141 } 142 return referenceFile.resolveSibling( referenceFile.getFileName( ) + "." + checksumAlgorithm.getDefaultExtension() ); 143 } 144 145 /** 146 * <p> 147 * Given a checksum file, check to see if the file it represents is valid according to the checksum. 148 * </p> 149 * <p> 150 * NOTE: Only supports single file checksums of type MD5 or SHA1. 151 * </p> 152 * 153 * @param algorithm the algorithms to check for. 154 * @return true if the checksum is valid for the file it represents. or if the checksum file does not exist. 155 * @throws IOException if the reading of the checksumFile or the file it refers to fails. 156 */ 157 public boolean isValidChecksum( ChecksumAlgorithm algorithm) throws ChecksumValidationException 158 { 159 return isValidChecksum( algorithm, false ); 160 } 161 public boolean isValidChecksum( ChecksumAlgorithm algorithm, boolean throwExceptions ) 162 throws ChecksumValidationException 163 { 164 return isValidChecksums( Arrays.asList( algorithm ), throwExceptions ); 165 } 166 167 /** 168 * Of any checksum files present, validate that the reference file conforms 169 * the to the checksum. 170 * 171 * @param algorithms the algorithms to check for. 172 * @return true if the checksums report that the the reference file is valid, false if invalid. 173 */ 174 public boolean isValidChecksums( List<ChecksumAlgorithm> algorithms) throws ChecksumValidationException 175 { 176 return isValidChecksums( algorithms, false ); 177 } 178 179 /** 180 * Checks if the checksum files are valid for the referenced file. 181 * It tries to find a checksum file for each algorithm in the same directory as the referenceFile. 182 * The method returns true, if at least one checksum file exists for one of the given algorithms 183 * and all existing checksum files are valid. 184 * 185 * This method throws only exceptions, if throwExceptions is true. Otherwise false will be returned instead. 186 * 187 * It verifies only the existing checksum files. If the checksum file for a particular algorithm does not exist, 188 * but others exist and are valid, it will return true. 189 * 190 * @param algorithms The algorithms to verify 191 * @param throwExceptions If true, exceptions will be thrown, otherwise false will be returned, if a exception occurred. 192 * @return True, if it is valid for all existing checksum files, otherwise false. 193 * @throws ChecksumValidationException 194 */ 195 public boolean isValidChecksums( List<ChecksumAlgorithm> algorithms, boolean throwExceptions) throws ChecksumValidationException 196 { 197 198 List<Checksum> checksums; 199 // Parse file once, for all checksums. 200 try 201 { 202 checksums = ChecksumUtil.initializeChecksums( referenceFile, algorithms ); 203 } 204 catch (IOException e ) 205 { 206 log.warn( "Unable to update checksum:{}", e.getMessage( ) ); 207 if (throwExceptions) { 208 if (e instanceof FileNotFoundException) { 209 throw new ChecksumValidationException(ChecksumValidationException.ValidationError.FILE_NOT_FOUND, e); 210 } else { 211 throw new ChecksumValidationException(ChecksumValidationException.ValidationError.READ_ERROR, e); 212 } 213 } else { 214 return false; 215 } 216 } 217 218 boolean valid = true; 219 boolean fileExists = false; 220 221 // No file exists -> return false 222 // if at least one file exists: 223 // -> all existing files must be valid 224 225 // check the checksum files 226 try 227 { 228 229 for ( Checksum checksum : checksums ) 230 { 231 ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm( ); 232 Path checksumFile = getChecksumFile( checksumAlgorithm ); 233 234 if (Files.exists(checksumFile)) { 235 fileExists = true; 236 String expectedChecksum = parseChecksum(checksumFile, checksumAlgorithm, referenceFile.getFileName().toString(), FILE_ENCODING); 237 238 valid &= checksum.compare(expectedChecksum); 239 } 240 } 241 } 242 catch ( ChecksumValidationException e ) 243 { 244 log.warn( "Unable to read / parse checksum: {}", e.getMessage( ) ); 245 if (throwExceptions) { 246 throw e; 247 } else 248 { 249 return false; 250 } 251 } 252 253 return fileExists && valid; 254 } 255 256 public Path getReferenceFile( ) 257 { 258 return referenceFile; 259 } 260 261 262 263 public UpdateStatusList fixChecksum(ChecksumAlgorithm algorithm) { 264 return fixChecksums( Arrays.asList(algorithm) ); 265 } 266 267 /** 268 * Writes a checksum file, if it does not exist or if it exists and has a different 269 * checksum value. 270 * 271 * @param algorithms the hashes to check for. 272 * @return true if checksums were created successfully. 273 */ 274 public UpdateStatusList fixChecksums( List<ChecksumAlgorithm> algorithms ) 275 { 276 UpdateStatusList result = UpdateStatusList.INITIALIZE(algorithms); 277 List<Checksum> checksums; 278 279 280 try 281 { 282 // Parse file once, for all checksums. 283 checksums = ChecksumUtil.initializeChecksums(getReferenceFile(), algorithms); 284 } 285 catch (IOException e ) 286 { 287 log.warn( e.getMessage( ), e ); 288 result.setTotalError(e); 289 return result; 290 } 291 // Any checksums? 292 if ( checksums.isEmpty( ) ) 293 { 294 // No checksum objects, no checksum files, default to is valid. 295 return result; 296 } 297 298 boolean valid = true; 299 300 // check the hash files 301 for ( Checksum checksum : checksums ) 302 { 303 ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm( ); 304 try 305 { 306 Path checksumFile = getChecksumFile( checksumAlgorithm ); 307 if ( Files.exists( checksumFile ) ) 308 { 309 String expectedChecksum; 310 try 311 { 312 expectedChecksum = parseChecksum( checksumFile, checksumAlgorithm, referenceFile.getFileName( ).toString( ), FILE_ENCODING ); 313 } catch (ChecksumValidationException ex) { 314 expectedChecksum = ""; 315 } 316 317 if ( !checksum.compare( expectedChecksum ) ) 318 { 319 // overwrite checksum file 320 writeChecksumFile( checksumFile, FILE_ENCODING, checksum.getChecksum( ) ); 321 result.setStatus(checksumAlgorithm,UpdateStatus.UPDATED); 322 } 323 } 324 else 325 { 326 writeChecksumFile( checksumFile, FILE_ENCODING, checksum.getChecksum( ) ); 327 result.setStatus(checksumAlgorithm, UpdateStatus.CREATED); 328 } 329 } 330 catch ( ChecksumValidationException e ) 331 { 332 log.warn( e.getMessage( ), e ); 333 result.setErrorStatus(checksumAlgorithm, e); 334 } 335 } 336 337 return result; 338 339 } 340 341 private void writeChecksumFile( Path checksumFile, Charset encoding, String checksumHex ) 342 { 343 FileUtils.writeStringToFile( checksumFile, FILE_ENCODING, checksumHex + " " + referenceFile.getFileName( ).toString( ) ); 344 } 345 346 private boolean isValidChecksumPattern( String filename, String path ) 347 { 348 // check if it is a remote metadata file 349 350 Matcher m = METADATA_PATTERN.matcher( path ); 351 if ( m.matches( ) ) 352 { 353 return filename.endsWith( path ) || ( "-".equals( filename ) ) || filename.endsWith( "maven-metadata.xml" ); 354 } 355 356 return filename.endsWith( path ) || ( "-".equals( filename ) ); 357 } 358 359 /** 360 * Parse a checksum string. 361 * <p> 362 * Validate the expected path, and expected checksum algorithm, then return 363 * the trimmed checksum hex string. 364 * </p> 365 * 366 * @param checksumFile The file where the checksum is stored 367 * @param checksumAlgorithm The checksum algorithm to check 368 * @param fileName The filename of the reference file 369 * @return 370 * @throws IOException 371 */ 372 public String parseChecksum( Path checksumFile, ChecksumAlgorithm checksumAlgorithm, String fileName, Charset encoding ) 373 throws ChecksumValidationException 374 { 375 ChecksumFileContent fc = parseChecksumFile( checksumFile, checksumAlgorithm, encoding ); 376 if ( fc.isFormatMatch() && !isValidChecksumPattern( fc.getFileReference( ), fileName ) ) 377 { 378 throw new ChecksumValidationException(BAD_CHECKSUM_FILE_REF, 379 "The file reference '" + fc.getFileReference( ) + "' in the checksum file does not match expected file: '" + fileName + "'" ); 380 } else if (!fc.isFormatMatch()) { 381 throw new ChecksumValidationException( BAD_CHECKSUM_FILE, "The checksum file content could not be parsed: "+checksumFile ); 382 } 383 return fc.getChecksum( ); 384 385 } 386 public ChecksumFileContent parseChecksumFile( Path checksumFile, ChecksumAlgorithm checksumAlgorithm, Charset encoding ) 387 { 388 ChecksumFileContent fc = new ChecksumFileContent( ); 389 String rawChecksumString = FileUtils.readFileToString( checksumFile, encoding ); 390 String trimmedChecksum = rawChecksumString.replace( '\n', ' ' ).trim( ); 391 392 // Free-BSD / openssl 393 String regex = checksumAlgorithm.getType( ) + "\\s*\\(([^)]*)\\)\\s*=\\s*([a-fA-F0-9]+)"; 394 Matcher m = Pattern.compile( regex ).matcher( trimmedChecksum ); 395 if ( m.matches( ) ) 396 { 397 fc.setFileReference( m.group( 1 ) ); 398 fc.setChecksum( m.group( 2 ) ); 399 fc.setFormatMatch( true ); 400 } 401 else 402 { 403 // GNU tools 404 m = Pattern.compile( "([a-fA-F0-9]+)\\s+\\*?(.+)" ).matcher( trimmedChecksum ); 405 if ( m.matches( ) ) 406 { 407 fc.setFileReference( m.group( 2 ) ); 408 fc.setChecksum( m.group( 1 ) ); 409 fc.setFormatMatch( true ); 410 } 411 else 412 { 413 fc.setFileReference( "" ); 414 fc.setChecksum( trimmedChecksum ); 415 fc.setFormatMatch( false ); 416 } 417 } 418 return fc; 419 } 420}