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