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