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