package org.apache.archiva.checksum; /* * 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. */ import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ChecksummedFile * <p>Terminology:</p> * <dl> * <dt>Checksum File</dt> * <dd>The file that contains the previously calculated checksum value for the reference file. * This is a text file with the extension ".sha1" or ".md5", and contains a single entry * consisting of an optional reference filename, and a checksum string. * </dd> * <dt>Reference File</dt> * <dd>The file that is being referenced in the checksum file.</dd> * </dl> */ public class ChecksummedFile { private final Logger log = LoggerFactory.getLogger( ChecksummedFile.class ); private static final Pattern METADATA_PATTERN = Pattern.compile( "maven-metadata-\\S*.xml" ); private final File referenceFile; /** * Construct a ChecksummedFile object. * * @param referenceFile */ public ChecksummedFile( final File referenceFile ) { this.referenceFile = referenceFile; } /** * Calculate the checksum based on a given checksum. * * @param checksumAlgorithm the algorithm to use. * @return the checksum string for the file. * @throws IOException if unable to calculate the checksum. */ public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm ) throws IOException { try (InputStream fis = Files.newInputStream( referenceFile.toPath() )) { Checksum checksum = new Checksum( checksumAlgorithm ); checksum.update( fis ); return checksum.getChecksum(); } } /** * Creates a checksum file of the provided referenceFile. * * @param checksumAlgorithm the hash to use. * @return the checksum File that was created. * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file. */ public File createChecksum( ChecksumAlgorithm checksumAlgorithm ) throws IOException { File checksumFile = new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() ); Files.deleteIfExists( checksumFile.toPath() ); String checksum = calculateChecksum( checksumAlgorithm ); Files.write( checksumFile.toPath(), // ( checksum + " " + referenceFile.getName() ).getBytes(), // StandardOpenOption.CREATE_NEW ); return checksumFile; } /** * Get the checksum file for the reference file and hash. * * @param checksumAlgorithm the hash that we are interested in. * @return the checksum file to return */ public File getChecksumFile( ChecksumAlgorithm checksumAlgorithm ) { return new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() ); } /** * <p> * Given a checksum file, check to see if the file it represents is valid according to the checksum. * </p> * <p> * NOTE: Only supports single file checksums of type MD5 or SHA1. * </p> * * @param algorithm the algorithms to check for. * @return true if the checksum is valid for the file it represents. or if the checksum file does not exist. * @throws IOException if the reading of the checksumFile or the file it refers to fails. */ public boolean isValidChecksum( ChecksumAlgorithm algorithm ) throws IOException { return isValidChecksums( new ChecksumAlgorithm[]{ algorithm } ); } /** * Of any checksum files present, validate that the reference file conforms * the to the checksum. * * @param algorithms the algorithms to check for. * @return true if the checksums report that the the reference file is valid, false if invalid. */ public boolean isValidChecksums( ChecksumAlgorithm algorithms[] ) { try (InputStream fis = Files.newInputStream( referenceFile.toPath() )) { List<Checksum> checksums = new ArrayList<>( algorithms.length ); // Create checksum object for each algorithm. for ( ChecksumAlgorithm checksumAlgorithm : algorithms ) { File checksumFile = getChecksumFile( checksumAlgorithm ); // Only add algorithm if checksum file exists. if ( checksumFile.exists() ) { checksums.add( new Checksum( checksumAlgorithm ) ); } } // Any checksums? if ( checksums.isEmpty() ) { // No checksum objects, no checksum files, default to is invalid. return false; } // Parse file once, for all checksums. try { Checksum.update( checksums, fis ); } catch ( IOException e ) { log.warn( "Unable to update checksum:{}", e.getMessage() ); return false; } boolean valid = true; // check the checksum files try { for ( Checksum checksum : checksums ) { ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm(); File checksumFile = getChecksumFile( checksumAlgorithm ); String rawChecksum = FileUtils.readFileToString( checksumFile ); String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() ); if ( !StringUtils.equalsIgnoreCase( expectedChecksum, checksum.getChecksum() ) ) { valid = false; } } } catch ( IOException e ) { log.warn( "Unable to read / parse checksum: {}", e.getMessage() ); return false; } return valid; } catch ( IOException e ) { log.warn( "Unable to read / parse checksum: {}", e.getMessage() ); return false; } } /** * Fix or create checksum files for the reference file. * * @param algorithms the hashes to check for. * @return true if checksums were created successfully. */ public boolean fixChecksums( ChecksumAlgorithm[] algorithms ) { List<Checksum> checksums = new ArrayList<>( algorithms.length ); // Create checksum object for each algorithm. for ( ChecksumAlgorithm checksumAlgorithm : algorithms ) { checksums.add( new Checksum( checksumAlgorithm ) ); } // Any checksums? if ( checksums.isEmpty() ) { // No checksum objects, no checksum files, default to is valid. return true; } try (InputStream fis = Files.newInputStream( referenceFile.toPath() )) { // Parse file once, for all checksums. Checksum.update( checksums, fis ); } catch ( IOException e ) { log.warn( e.getMessage(), e ); return false; } boolean valid = true; // check the hash files for ( Checksum checksum : checksums ) { ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm(); try { File checksumFile = getChecksumFile( checksumAlgorithm ); String actualChecksum = checksum.getChecksum(); if ( checksumFile.exists() ) { String rawChecksum = FileUtils.readFileToString( checksumFile ); String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() ); if ( !StringUtils.equalsIgnoreCase( expectedChecksum, actualChecksum ) ) { // create checksum (again) FileUtils.writeStringToFile( checksumFile, actualChecksum + " " + referenceFile.getName() ); } } else { FileUtils.writeStringToFile( checksumFile, actualChecksum + " " + referenceFile.getName() ); } } catch ( IOException e ) { log.warn( e.getMessage(), e ); valid = false; } } return valid; } private boolean isValidChecksumPattern( String filename, String path ) { // check if it is a remote metadata file Matcher m = METADATA_PATTERN.matcher( path ); if ( m.matches() ) { return filename.endsWith( path ) || ( "-".equals( filename ) ) || filename.endsWith( "maven-metadata.xml" ); } return filename.endsWith( path ) || ( "-".equals( filename ) ); } /** * Parse a checksum string. * <p> * Validate the expected path, and expected checksum algorithm, then return * the trimmed checksum hex string. * </p> * * @param rawChecksumString * @param expectedHash * @param expectedPath * @return * @throws IOException */ public String parseChecksum( String rawChecksumString, ChecksumAlgorithm expectedHash, String expectedPath ) throws IOException { String trimmedChecksum = rawChecksumString.replace( '\n', ' ' ).trim(); // Free-BSD / openssl String regex = expectedHash.getType() + "\\s*\\(([^)]*)\\)\\s*=\\s*([a-fA-F0-9]+)"; Matcher m = Pattern.compile( regex ).matcher( trimmedChecksum ); if ( m.matches() ) { String filename = m.group( 1 ); if ( !isValidChecksumPattern( filename, expectedPath ) ) { throw new IOException( "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath + "'" ); } trimmedChecksum = m.group( 2 ); } else { // GNU tools m = Pattern.compile( "([a-fA-F0-9]+)\\s+\\*?(.+)" ).matcher( trimmedChecksum ); if ( m.matches() ) { String filename = m.group( 2 ); if ( !isValidChecksumPattern( filename, expectedPath ) ) { throw new IOException( "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath + "'" ); } trimmedChecksum = m.group( 1 ); } } return trimmedChecksum; } }