]> source.dussan.org Git - archiva.git/blob
89181b5fd512759128b5c902e4d6312ce900670d
[archiva.git] /
1 package org.apache.archiva.checksum;
2
3 /*
4  * Licensed to the Apache Software Foundation (ASF) under one
5  * or more contributor license agreements.  See the NOTICE file
6  * distributed with this work for additional information
7  * regarding copyright ownership.  The ASF licenses this file
8  * to you under the Apache License, Version 2.0 (the
9  * "License"); you may not use this file except in compliance
10  * with the License.  You may obtain a copy of the License at
11  *
12  *   http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  * KIND, either express or implied.  See the License for the
18  * specific language governing permissions and limitations
19  * under the License.
20  */
21
22 import org.apache.commons.io.FileUtils;
23 import org.apache.commons.lang.StringUtils;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.nio.file.Files;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36
37 /**
38  * ChecksummedFile
39  *
40  * <p>Terminology:</p>
41  * <dl>
42  * <dt>Checksum File</dt>
43  * <dd>The file that contains the previously calculated checksum value for the reference file.
44  * This is a text file with the extension ".sha1" or ".md5", and contains a single entry
45  * consisting of an optional reference filename, and a checksum string.
46  * </dd>
47  * <dt>Reference File</dt>
48  * <dd>The file that is being referenced in the checksum file.</dd>
49  * </dl>
50  *
51  *
52  */
53 public class ChecksummedFile
54 {
55     private Logger log = LoggerFactory.getLogger( ChecksummedFile.class );
56
57     private final File referenceFile;
58
59     /**
60      * Construct a ChecksummedFile object.
61      *
62      * @param referenceFile
63      */
64     public ChecksummedFile( final File referenceFile )
65     {
66         this.referenceFile = referenceFile;
67     }
68
69     /**
70      * Calculate the checksum based on a given checksum.
71      *
72      * @param checksumAlgorithm the algorithm to use.
73      * @return the checksum string for the file.
74      * @throws IOException if unable to calculate the checksum.
75      */
76     public String calculateChecksum( ChecksumAlgorithm checksumAlgorithm )
77         throws IOException
78     {
79
80         try (InputStream fis = Files.newInputStream( referenceFile.toPath() ) )
81         {
82             Checksum checksum = new Checksum( checksumAlgorithm );
83             checksum.update( fis );
84             return checksum.getChecksum();
85         }
86     }
87
88     /**
89      * Creates a checksum file of the provided referenceFile.
90      *
91      * @param checksumAlgorithm the hash to use.
92      * @return the checksum File that was created.
93      * @throws IOException if there was a problem either reading the referenceFile, or writing the checksum file.
94      */
95     public File createChecksum( ChecksumAlgorithm checksumAlgorithm )
96         throws IOException
97     {
98         File checksumFile = new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() );
99         String checksum = calculateChecksum( checksumAlgorithm );
100         FileUtils.writeStringToFile( checksumFile, checksum + "  " + referenceFile.getName() );
101         return checksumFile;
102     }
103
104     /**
105      * Get the checksum file for the reference file and hash.
106      *
107      * @param checksumAlgorithm the hash that we are interested in.
108      * @return the checksum file to return
109      */
110     public File getChecksumFile( ChecksumAlgorithm checksumAlgorithm )
111     {
112         return new File( referenceFile.getAbsolutePath() + "." + checksumAlgorithm.getExt() );
113     }
114
115     /**
116      * <p>
117      * Given a checksum file, check to see if the file it represents is valid according to the checksum.
118      * </p>
119      *
120      * <p>
121      * NOTE: Only supports single file checksums of type MD5 or SHA1.
122      * </p>
123      *
124      * @param algorithm the algorithms to check for.
125      * @return true if the checksum is valid for the file it represents. or if the checksum file does not exist.
126      * @throws IOException if the reading of the checksumFile or the file it refers to fails.
127      */
128     public boolean isValidChecksum( ChecksumAlgorithm algorithm )
129         throws IOException
130     {
131         return isValidChecksums( new ChecksumAlgorithm[]{ algorithm } );
132     }
133
134     /**
135      * Of any checksum files present, validate that the reference file conforms
136      * the to the checksum.
137      *
138      * @param algorithms the algorithms to check for.
139      * @return true if the checksums report that the the reference file is valid, false if invalid.
140      */
141     public boolean isValidChecksums( ChecksumAlgorithm algorithms[] )
142     {
143
144         try (InputStream fis = Files.newInputStream( referenceFile.toPath() ))
145         {
146             List<Checksum> checksums = new ArrayList<>( algorithms.length );
147             // Create checksum object for each algorithm.
148             for ( ChecksumAlgorithm checksumAlgorithm : algorithms )
149             {
150                 File checksumFile = getChecksumFile( checksumAlgorithm );
151
152                 // Only add algorithm if checksum file exists.
153                 if ( checksumFile.exists() )
154                 {
155                     checksums.add( new Checksum( checksumAlgorithm ) );
156                 }
157             }
158
159             // Any checksums?
160             if ( checksums.isEmpty() )
161             {
162                 // No checksum objects, no checksum files, default to is invalid.
163                 return false;
164             }
165
166             // Parse file once, for all checksums.
167             try
168             {
169                 Checksum.update( checksums, fis );
170             }
171             catch ( IOException e )
172             {
173                 log.warn( "Unable to update checksum:{}", e.getMessage() );
174                 return false;
175             }
176
177             boolean valid = true;
178
179             // check the checksum files
180             try
181             {
182                 for ( Checksum checksum : checksums )
183                 {
184                     ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm();
185                     File checksumFile = getChecksumFile( checksumAlgorithm );
186
187                     String rawChecksum = FileUtils.readFileToString( checksumFile );
188                     String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() );
189
190                     if ( !StringUtils.equalsIgnoreCase( expectedChecksum, checksum.getChecksum() ) )
191                     {
192                         valid = false;
193                     }
194                 }
195             }
196             catch ( IOException e )
197             {
198                 log.warn( "Unable to read / parse checksum: {}", e.getMessage() );
199                 return false;
200             }
201
202             return valid;
203         } catch ( IOException e )
204         {
205             log.warn( "Unable to read / parse checksum: {}", e.getMessage() );
206             return false;
207         }
208     }
209
210     /**
211      * Fix or create checksum files for the reference file.
212      *
213      * @param algorithms the hashes to check for.
214      * @return true if checksums were created successfully.
215      */
216     public boolean fixChecksums( ChecksumAlgorithm[] algorithms )
217     {
218         List<Checksum> checksums = new ArrayList<>( algorithms.length );
219         // Create checksum object for each algorithm.
220         for ( ChecksumAlgorithm checksumAlgorithm : algorithms )
221         {
222             checksums.add( new Checksum( checksumAlgorithm ) );
223         }
224
225         // Any checksums?
226         if ( checksums.isEmpty() )
227         {
228             // No checksum objects, no checksum files, default to is valid.
229             return true;
230         }
231
232
233         try (InputStream fis = Files.newInputStream( referenceFile.toPath() ))
234         {
235             // Parse file once, for all checksums.
236             Checksum.update( checksums, fis );
237         }
238         catch ( IOException e )
239         {
240             log.warn( e.getMessage(), e );
241             return false;
242         }
243
244         boolean valid = true;
245
246         // check the hash files
247         for ( Checksum checksum : checksums )
248         {
249             ChecksumAlgorithm checksumAlgorithm = checksum.getAlgorithm();
250             try
251             {
252                 File checksumFile = getChecksumFile( checksumAlgorithm );
253                 String actualChecksum = checksum.getChecksum();
254
255                 if ( checksumFile.exists() )
256                 {
257                     String rawChecksum = FileUtils.readFileToString( checksumFile );
258                     String expectedChecksum = parseChecksum( rawChecksum, checksumAlgorithm, referenceFile.getName() );
259
260                     if ( !StringUtils.equalsIgnoreCase( expectedChecksum, actualChecksum ) )
261                     {
262                         // create checksum (again)
263                         FileUtils.writeStringToFile( checksumFile, actualChecksum + "  " + referenceFile.getName() );
264                     }
265                 }
266                 else
267                 {
268                     FileUtils.writeStringToFile( checksumFile, actualChecksum + "  " + referenceFile.getName() );
269                 }
270             }
271             catch ( IOException e )
272             {
273                 log.warn( e.getMessage(), e );
274                 valid = false;
275             }
276         }
277
278         return valid;
279
280     }
281
282     private boolean isValidChecksumPattern( String filename, String path )
283     {
284         // check if it is a remote metadata file
285         Pattern pattern = Pattern.compile( "maven-metadata-\\S*.xml" );
286         Matcher m = pattern.matcher( path );
287         if ( m.matches() )
288         {
289             return filename.endsWith( path ) || ( "-".equals( filename ) ) || filename.endsWith( "maven-metadata.xml" );
290         }
291
292         return filename.endsWith( path ) || ( "-".equals( filename ) );
293     }
294
295     /**
296      * Parse a checksum string.
297      * <p>
298      * Validate the expected path, and expected checksum algorithm, then return
299      * the trimmed checksum hex string.
300      * </p>
301      * @param rawChecksumString
302      * @param expectedHash
303      * @param expectedPath
304      * @return
305      * @throws IOException
306      */
307     public String parseChecksum( String rawChecksumString, ChecksumAlgorithm expectedHash, String expectedPath )
308         throws IOException
309     {
310         String trimmedChecksum = rawChecksumString.replace( '\n', ' ' ).trim();
311
312         // Free-BSD / openssl
313         String regex = expectedHash.getType() + "\\s*\\(([^)]*)\\)\\s*=\\s*([a-fA-F0-9]+)";
314         Matcher m = Pattern.compile( regex ).matcher( trimmedChecksum );
315         if ( m.matches() )
316         {
317             String filename = m.group( 1 );
318             if ( !isValidChecksumPattern( filename, expectedPath ) )
319             {
320                 throw new IOException(
321                     "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath + "'" );
322             }
323             trimmedChecksum = m.group( 2 );
324         }
325         else
326         {
327             // GNU tools
328             m = Pattern.compile( "([a-fA-F0-9]+)\\s+\\*?(.+)" ).matcher( trimmedChecksum );
329             if ( m.matches() )
330             {
331                 String filename = m.group( 2 );
332                 if ( !isValidChecksumPattern( filename, expectedPath ) )
333                 {
334                     throw new IOException(
335                         "Supplied checksum file '" + filename + "' does not match expected file: '" + expectedPath
336                             + "'" );
337                 }
338                 trimmedChecksum = m.group( 1 );
339             }
340         }
341         return trimmedChecksum;
342     }
343 }