View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.connector.basic;
20  
21  import java.io.IOException;
22  import java.io.UncheckedIOException;
23  import java.net.URI;
24  import java.nio.file.Path;
25  import java.util.Collection;
26  import java.util.HashMap;
27  import java.util.Map;
28  
29  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
30  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
31  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy.ChecksumKind;
32  import org.eclipse.aether.spi.connector.layout.RepositoryLayout.ChecksumLocation;
33  import org.eclipse.aether.spi.io.ChecksumProcessor;
34  import org.eclipse.aether.spi.io.PathProcessor;
35  import org.eclipse.aether.transfer.ChecksumFailureException;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * Performs checksum validation for a downloaded file.
41   */
42  final class ChecksumValidator {
43  
44      interface ChecksumFetcher {
45  
46          /**
47           * Fetches the checksums from remote location into provided local file. The checksums fetched in this way
48           * are of kind {@link ChecksumKind#REMOTE_EXTERNAL}.
49           */
50          boolean fetchChecksum(URI remote, Path local) throws Exception;
51      }
52  
53      private static final Logger LOGGER = LoggerFactory.getLogger(ChecksumValidator.class);
54  
55      private final Path dataPath;
56  
57      private final Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories;
58  
59      private final PathProcessor pathProcessor;
60  
61      private final ChecksumProcessor checksumProcessor;
62  
63      private final ChecksumFetcher checksumFetcher;
64  
65      private final ChecksumPolicy checksumPolicy;
66  
67      private final Map<String, String> providedChecksums;
68  
69      private final Collection<ChecksumLocation> checksumLocations;
70  
71      private final Map<Path, String> checksumExpectedValues;
72  
73      @SuppressWarnings("checkstyle:parameternumber")
74      ChecksumValidator(
75              Path dataPath,
76              Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
77              PathProcessor pathProcessor,
78              ChecksumProcessor checksumProcessor,
79              ChecksumFetcher checksumFetcher,
80              ChecksumPolicy checksumPolicy,
81              Map<String, String> providedChecksums,
82              Collection<ChecksumLocation> checksumLocations) {
83          this.dataPath = dataPath;
84          this.checksumAlgorithmFactories = checksumAlgorithmFactories;
85          this.pathProcessor = pathProcessor;
86          this.checksumProcessor = checksumProcessor;
87          this.checksumFetcher = checksumFetcher;
88          this.checksumPolicy = checksumPolicy;
89          this.providedChecksums = providedChecksums;
90          this.checksumLocations = checksumLocations;
91          this.checksumExpectedValues = new HashMap<>();
92      }
93  
94      public ChecksumCalculator newChecksumCalculator(Path targetFile) {
95          if (checksumPolicy != null) {
96              return ChecksumCalculator.newInstance(targetFile, checksumAlgorithmFactories);
97          }
98          return null;
99      }
100 
101     public void validate(Map<String, ?> actualChecksums, Map<String, ?> includedChecksums)
102             throws ChecksumFailureException {
103         if (checksumPolicy == null) {
104             return;
105         }
106         if (providedChecksums != null && validateChecksums(actualChecksums, ChecksumKind.PROVIDED, providedChecksums)) {
107             return;
108         }
109         if (includedChecksums != null
110                 && validateChecksums(actualChecksums, ChecksumKind.REMOTE_INCLUDED, includedChecksums)) {
111             return;
112         }
113         if (!checksumLocations.isEmpty()) {
114             if (validateExternalChecksums(actualChecksums)) {
115                 return;
116             }
117             checksumPolicy.onNoMoreChecksums();
118         }
119     }
120 
121     private boolean validateChecksums(Map<String, ?> actualChecksums, ChecksumKind kind, Map<String, ?> checksums)
122             throws ChecksumFailureException {
123         boolean accepted = false;
124         boolean rejected = false;
125         for (Map.Entry<String, ?> entry : checksums.entrySet()) {
126             String algo = entry.getKey();
127             Object calculated = actualChecksums.get(algo);
128             if (!(calculated instanceof String)) {
129                 continue;
130             }
131             ChecksumAlgorithmFactory checksumAlgorithmFactory = checksumAlgorithmFactories.stream()
132                     .filter(a -> a.getName().equals(algo))
133                     .findFirst()
134                     .orElse(null);
135             if (checksumAlgorithmFactory == null) {
136                 continue;
137             }
138 
139             String actual = String.valueOf(calculated);
140             String expected = entry.getValue().toString();
141             checksumExpectedValues.put(getChecksumPath(checksumAlgorithmFactory), expected);
142 
143             if (!isEqualChecksum(expected, actual)) {
144                 // ALL comparable checksums of this kind must match: a mismatch on any algorithm rejects the
145                 // whole kind, so a match on a weaker algorithm (map iteration order is not strength-ordered)
146                 // cannot mask a recorded mismatch on a stronger one. The policy decides whether the mismatch
147                 // fails the transfer immediately (default) or is merely recorded.
148                 rejected = true;
149                 checksumPolicy.onChecksumMismatch(
150                         checksumAlgorithmFactory.getName(),
151                         kind,
152                         ChecksumFailureException.mismatch(expected, kind.name(), actual));
153             } else if (checksumPolicy.onChecksumMatch(checksumAlgorithmFactory.getName(), kind)) {
154                 accepted = true;
155             }
156         }
157         return accepted && !rejected;
158     }
159 
160     private boolean validateExternalChecksums(Map<String, ?> actualChecksums) throws ChecksumFailureException {
161         boolean rejected = false;
162         for (ChecksumLocation checksumLocation : checksumLocations) {
163             ChecksumAlgorithmFactory factory = checksumLocation.getChecksumAlgorithmFactory();
164             Object calculated = actualChecksums.get(factory.getName());
165             if (calculated instanceof Exception) {
166                 checksumPolicy.onChecksumError(
167                         factory.getName(),
168                         ChecksumKind.REMOTE_EXTERNAL,
169                         ChecksumFailureException.processingFailure(
170                                 "Checksum calculation problem", (Exception) calculated));
171                 continue;
172             }
173             Path checksumFile = getChecksumPath(checksumLocation.getChecksumAlgorithmFactory());
174             try (PathProcessor.TempFile tempFile = pathProcessor.newTempFile()) {
175                 Path tmp = tempFile.getPath();
176                 try {
177                     if (!checksumFetcher.fetchChecksum(checksumLocation.getLocation(), tmp)) {
178                         continue;
179                     }
180                 } catch (Exception e) {
181                     checksumPolicy.onChecksumError(
182                             factory.getName(),
183                             ChecksumKind.REMOTE_EXTERNAL,
184                             ChecksumFailureException.processingFailure("Checksum fetching problem", e));
185                     continue;
186                 }
187 
188                 String actual = String.valueOf(calculated);
189                 String expected = checksumProcessor.readChecksum(tmp);
190                 checksumExpectedValues.put(checksumFile, expected);
191 
192                 if (!isEqualChecksum(expected, actual)) {
193                     rejected = true;
194                     checksumPolicy.onChecksumMismatch(
195                             factory.getName(),
196                             ChecksumKind.REMOTE_EXTERNAL,
197                             ChecksumFailureException.mismatch(expected, ChecksumKind.REMOTE_EXTERNAL.name(), actual));
198                 } else if (checksumPolicy.onChecksumMatch(factory.getName(), ChecksumKind.REMOTE_EXTERNAL)
199                         && !rejected) {
200                     // a fetched checksum that mismatched earlier already rejected this download: a later
201                     // (possibly weaker) matching algorithm must not accept it
202                     return true;
203                 }
204             } catch (IOException e) {
205                 checksumPolicy.onChecksumError(
206                         factory.getName(),
207                         ChecksumKind.REMOTE_EXTERNAL,
208                         ChecksumFailureException.processingFailure("Checksum related IO problem", e));
209             }
210         }
211         return false;
212     }
213 
214     private static boolean isEqualChecksum(String expected, String actual) {
215         return expected.equalsIgnoreCase(actual);
216     }
217 
218     private Path getChecksumPath(ChecksumAlgorithmFactory factory) {
219         return dataPath.getParent().resolve(dataPath.getFileName() + "." + factory.getFileExtension());
220     }
221 
222     public void retry() {
223         checksumPolicy.onTransferRetry();
224         checksumExpectedValues.clear();
225     }
226 
227     public boolean handle(ChecksumFailureException exception) {
228         return checksumPolicy.onTransferChecksumFailure(exception);
229     }
230 
231     public void commit() {
232         for (Map.Entry<Path, String> entry : checksumExpectedValues.entrySet()) {
233             Path checksumFile = entry.getKey();
234             try {
235                 checksumProcessor.writeChecksum(checksumFile, entry.getValue());
236             } catch (IOException e) {
237                 LOGGER.debug("Failed to write checksum file {}", checksumFile, e);
238                 throw new UncheckedIOException(e);
239             }
240         }
241         checksumExpectedValues.clear();
242     }
243 }