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