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.commons.io.FileUtils; 023import org.apache.commons.lang.StringUtils; 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027import java.io.File; 028import java.io.IOException; 029import java.io.InputStream; 030import java.nio.file.Files; 031import java.nio.file.StandardOpenOption; 032import java.util.ArrayList; 033import java.util.List; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036 037/** 038 * ChecksummedFile 039 * <p>Terminology:</p> 040 * <dl> 041 * <dt>Checksum File</dt> 042 * <dd>The file that contains the previously calculated checksum value for the reference file. 043 * This is a text file with the extension ".sha1" or ".md5", and contains a single entry 044 * consisting of an optional reference filename, and a checksum string. 045 * </dd> 046 * <dt>Reference File</dt> 047 * <dd>The file that is being referenced in the checksum file.</dd> 048 * </dl> 049 */ 050public class ChecksummedFile 051{ 052 private final Logger log = LoggerFactory.getLogger( ChecksummedFile.class ); 053 054 private static final Pattern METADATA_PATTERN = Pattern.compile( "maven-metadata-\\S*.xml" ); 055 056 private final File referenceFile; 057 058 /** 059 * Construct a ChecksummedFile object. 060 * 061 * @param referenceFile 062 */ 063 public ChecksummedFile( final File referenceFile ) 064 { 065 this.referenceFile = referenceFile; 066 } 067 068 /** 069 * Calculate the checksum based on a given checksum. 070 * 071 * @param checksumAlgorithm the algorithm to use. 072 * @return the checksum string for the file. 073 * @throws IOException if unable to calculate the checksum. 074 */ 075 public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm ) 076 throws IOException 077 { 078 079 try (InputStream fis = Files.newInputStream( referenceFile.toPath() )) 080 { 081 Checksum checksum = new Checksum( checksumAlgorithm ); 082 checksum.update( fis ); 083 return checksum.getChecksum(); 084 } 085 } 086 087 /** 088 * Creates a checksum file of the provided referenceFile. 089 * 090 * @param checksumAlgorithm the hash to use. 091 * @return the checksum File that was created. 092 * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file. 093 */ 094 public File createChecksum( ChecksumAlgorithm checksumAlgorithm ) 095 throws IOException 096 { 097 File checksumFile = new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() ); 098 Files.deleteIfExists( checksumFile.toPath() ); 099 String checksum = calculateChecksum( checksumAlgorithm ); 100 Files.write( checksumFile.toPath(), // 101 ( checksum + " " + referenceFile.getName() ).getBytes(), // 102 StandardOpenOption.CREATE_NEW ); 103 return checksumFile; 104 } 105 106 /** 107 * Get the checksum file for the reference file and hash. 108 * 109 * @param checksumAlgorithm the hash that we are interested in. 110 * @return the checksum file to return 111 */ 112 public File getChecksumFile( ChecksumAlgorithm checksumAlgorithm ) 113 { 114 return new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() ); 115 } 116 117 /** 118 * <p> 119 * Given a checksum file, check to see if the file it represents is valid according to the checksum. 120 * </p> 121 * <p> 122 * NOTE: Only supports single file checksums of type MD5 or SHA1. 123 * </p> 124 * 125 * @param algorithm the algorithms to check for. 126 * @return true if the checksum is valid for the file it represents. or if the checksum file does not exist. 127 * @throws IOException if the reading of the checksumFile or the file it refers to fails. 128 */ 129 public boolean isValidChecksum( ChecksumAlgorithm algorithm ) 130 throws IOException 131 { 132 return isValidChecksums( new ChecksumAlgorithm[]{ algorithm } ); 133 } 134 135 /** 136 * Of any checksum files present, validate that the reference file conforms 137 * the to the checksum. 138 * 139 * @param algorithms the algorithms to check for. 140 * @return true if the checksums report that the the reference file is valid, false if invalid. 141 */ 142 public boolean isValidChecksums( ChecksumAlgorithm algorithms[] ) 143 { 144 145 try (InputStream fis = Files.newInputStream( referenceFile.toPath() )) 146 { 147 List<Checksum> checksums = new ArrayList<>( algorithms.length ); 148 // Create checksum object for each algorithm. 149 for ( ChecksumAlgorithm checksumAlgorithm : algorithms ) 150 { 151 File checksumFile = getChecksumFile( checksumAlgorithm ); 152 153 // Only add algorithm if checksum file exists. 154 if ( checksumFile.exists() ) 155 { 156 checksums.add( new Checksum( checksumAlgorithm ) ); 157 } 158 } 159 160 // Any checksums? 161 if ( checksums.isEmpty() ) 162 { 163 // No checksum objects, no checksum files, default to is invalid. 164 return false; 165 } 166 167 // Parse file once, for all checksums. 168 try 169 { 170 Checksum.update( checksums, fis ); 171 } 172 catch ( IOException e ) 173 { 174 log.warn( "Unable to update checksum:{}", e.getMessage() ); 175 return false; 176 } 177 178 boolean valid = true; 179 180 // check the checksum files 181 try 182 { 183 for ( Checksum checksum : checksums ) 184 { 185 ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm(); 186 File checksumFile = getChecksumFile( checksumAlgorithm ); 187 188 String rawChecksum = FileUtils.readFileToString( checksumFile ); 189 String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() ); 190 191 if ( !StringUtils.equalsIgnoreCase( expectedChecksum, checksum.getChecksum() ) ) 192 { 193 valid = false; 194 } 195 } 196 } 197 catch ( IOException e ) 198 { 199 log.warn( "Unable to read / parse checksum: {}", e.getMessage() ); 200 return false; 201 } 202 203 return valid; 204 } 205 catch ( IOException e ) 206 { 207 log.warn( "Unable to read / parse checksum: {}", e.getMessage() ); 208 return false; 209 } 210 } 211 212 /** 213 * Fix or create checksum files for the reference file. 214 * 215 * @param algorithms the hashes to check for. 216 * @return true if checksums were created successfully. 217 */ 218 public boolean fixChecksums( ChecksumAlgorithm[] algorithms ) 219 { 220 List<Checksum> checksums = new ArrayList<>( algorithms.length ); 221 // Create checksum object for each algorithm. 222 for ( ChecksumAlgorithm checksumAlgorithm : algorithms ) 223 { 224 checksums.add( new Checksum( checksumAlgorithm ) ); 225 } 226 227 // Any checksums? 228 if ( checksums.isEmpty() ) 229 { 230 // No checksum objects, no checksum files, default to is valid. 231 return true; 232 } 233 234 try (InputStream fis = Files.newInputStream( referenceFile.toPath() )) 235 { 236 // Parse file once, for all checksums. 237 Checksum.update( checksums, fis ); 238 } 239 catch ( IOException e ) 240 { 241 log.warn( e.getMessage(), e ); 242 return false; 243 } 244 245 boolean valid = true; 246 247 // check the hash files 248 for ( Checksum checksum : checksums ) 249 { 250 ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm(); 251 try 252 { 253 File checksumFile = getChecksumFile( checksumAlgorithm ); 254 String actualChecksum = checksum.getChecksum(); 255 256 if ( checksumFile.exists() ) 257 { 258 String rawChecksum = FileUtils.readFileToString( checksumFile ); 259 String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() ); 260 261 if ( !StringUtils.equalsIgnoreCase( expectedChecksum, actualChecksum ) ) 262 { 263 // create checksum (again) 264 FileUtils.writeStringToFile( checksumFile, actualChecksum + " " + referenceFile.getName() ); 265 } 266 } 267 else 268 { 269 FileUtils.writeStringToFile( checksumFile, actualChecksum + " " + referenceFile.getName() ); 270 } 271 } 272 catch ( IOException e ) 273 { 274 log.warn( e.getMessage(), e ); 275 valid = false; 276 } 277 } 278 279 return valid; 280 281 } 282 283 private boolean isValidChecksumPattern( String filename, String path ) 284 { 285 // check if it is a remote metadata file 286 287 Matcher m = METADATA_PATTERN.matcher( path ); 288 if ( m.matches() ) 289 { 290 return filename.endsWith( path ) || ( "-".equals( filename ) ) || filename.endsWith( "maven-metadata.xml" ); 291 } 292 293 return filename.endsWith( path ) || ( "-".equals( filename ) ); 294 } 295 296 /** 297 * Parse a checksum string. 298 * <p> 299 * Validate the expected path, and expected checksum algorithm, then return 300 * the trimmed checksum hex string. 301 * </p> 302 * 303 * @param rawChecksumString 304 * @param expectedHash 305 * @param expectedPath 306 * @return 307 * @throws IOException 308 */ 309 public String parseChecksum( String rawChecksumString, ChecksumAlgorithm expectedHash, String expectedPath ) 310 throws IOException 311 { 312 String trimmedChecksum = rawChecksumString.replace( '\n', ' ' ).trim(); 313 314 // Free-BSD / openssl 315 String regex = expectedHash.getType() + "\\s*\\(([^)]*)\\)\\s*=\\s*([a-fA-F0-9]+)"; 316 Matcher m = Pattern.compile( regex ).matcher( trimmedChecksum ); 317 if ( m.matches() ) 318 { 319 String filename = m.group( 1 ); 320 if ( !isValidChecksumPattern( filename, expectedPath ) ) 321 { 322 throw new IOException( 323 "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath + "'" ); 324 } 325 trimmedChecksum = m.group( 2 ); 326 } 327 else 328 { 329 // GNU tools 330 m = Pattern.compile( "([a-fA-F0-9]+)\\s+\\*?(.+)" ).matcher( trimmedChecksum ); 331 if ( m.matches() ) 332 { 333 String filename = m.group( 2 ); 334 if ( !isValidChecksumPattern( filename, expectedPath ) ) 335 { 336 throw new IOException( 337 "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath 338 + "'" ); 339 } 340 trimmedChecksum = m.group( 1 ); 341 } 342 } 343 return trimmedChecksum; 344 } 345}