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.internal.impl;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.ListIterator;
28  import java.util.Map;
29  import java.util.concurrent.ConcurrentHashMap;
30  import java.util.stream.Collectors;
31  
32  import org.eclipse.aether.ConfigurationProperties;
33  import org.eclipse.aether.Keys;
34  import org.eclipse.aether.RepositoryCache;
35  import org.eclipse.aether.RepositorySystemSession;
36  import org.eclipse.aether.impl.RemoteRepositoryManager;
37  import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
38  import org.eclipse.aether.repository.Authentication;
39  import org.eclipse.aether.repository.AuthenticationSelector;
40  import org.eclipse.aether.repository.MirrorSelector;
41  import org.eclipse.aether.repository.Proxy;
42  import org.eclipse.aether.repository.ProxySelector;
43  import org.eclipse.aether.repository.RemoteRepository;
44  import org.eclipse.aether.repository.RepositoryKeyFunction;
45  import org.eclipse.aether.repository.RepositoryPolicy;
46  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
47  import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
48  import org.eclipse.aether.util.ConfigUtils;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import static java.util.Objects.requireNonNull;
53  
54  /**
55   */
56  @Singleton
57  @Named
58  public class DefaultRemoteRepositoryManager implements RemoteRepositoryManager {
59  
60      private static final String CONFIG_PROPS_PREFIX =
61              ConfigurationProperties.PREFIX_AETHER + "remoteRepositoryManager.";
62  
63      /**
64       * Flag indicating whether session authentication (i.e. credentials configured in {@code settings.xml}) may be
65       * applied, matched by plain repository ID, to repositories declared by remote artifact descriptors (POMs) that
66       * are merged into the effective repository list during dependency collection. When disabled (the default),
67       * session authentication is only applied to such a repository when an operator-defined mirror has been selected
68       * for it; if credentials would have matched a descriptor-declared repository, a warning naming the repository ID
69       * and URL is logged instead. Repositories supplied by the build itself (e.g. aggregated via
70       * {@code RepositorySystem#newResolutionRepositories}) are unaffected and keep receiving matching credentials.
71       * Enabling this restores the legacy behavior of applying matching session authentication to descriptor
72       * declared repositories regardless of their provenance.
73       *
74       * @since 2.0.23
75       * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
76       * @configurationType {@link java.lang.Boolean}
77       * @configurationDefaultValue {@link #DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES}
78       */
79      public static final String CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES =
80              CONFIG_PROPS_PREFIX + "authToDescriptorRepositories";
81  
82      public static final boolean DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES = false;
83  
84      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRemoteRepositoryManager.class);
85  
86      private final UpdatePolicyAnalyzer updatePolicyAnalyzer;
87  
88      private final ChecksumPolicyProvider checksumPolicyProvider;
89  
90      private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
91  
92      @Inject
93      public DefaultRemoteRepositoryManager(
94              UpdatePolicyAnalyzer updatePolicyAnalyzer,
95              ChecksumPolicyProvider checksumPolicyProvider,
96              RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) {
97          this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
98          this.checksumPolicyProvider = requireNonNull(checksumPolicyProvider, "checksum policy provider cannot be null");
99          this.repositoryKeyFunctionFactory =
100                 requireNonNull(repositoryKeyFunctionFactory, "repository key function factory cannot be null");
101     }
102 
103     @Override
104     public List<RemoteRepository> aggregateRepositories(
105             RepositorySystemSession session,
106             List<RemoteRepository> dominantRepositories,
107             List<RemoteRepository> recessiveRepositories,
108             boolean recessiveIsRaw) {
109         return aggregateRepositories(session, dominantRepositories, recessiveRepositories, recessiveIsRaw, false);
110     }
111 
112     @Override
113     public List<RemoteRepository> aggregateRepositories(
114             RepositorySystemSession session,
115             List<RemoteRepository> dominantRepositories,
116             List<RemoteRepository> recessiveRepositories,
117             boolean recessiveIsRaw,
118             boolean recessiveIsFromDescriptor) {
119         requireNonNull(session, "session cannot be null");
120         requireNonNull(dominantRepositories, "dominantRepositories cannot be null");
121         requireNonNull(recessiveRepositories, "recessiveRepositories cannot be null");
122         if (recessiveRepositories.isEmpty()) {
123             return dominantRepositories;
124         }
125 
126         RepositoryKeyFunction repositoryKeyFunction = repositoryKeyFunctionFactory.systemRepositoryKeyFunction(session);
127         MirrorSelector mirrorSelector = session.getMirrorSelector();
128         AuthenticationSelector authSelector = session.getAuthenticationSelector();
129         ProxySelector proxySelector = session.getProxySelector();
130 
131         boolean authToDescriptorRepositories = ConfigUtils.getBoolean(
132                 session, DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES, CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES);
133 
134         List<RemoteRepository> result = new ArrayList<>(dominantRepositories);
135 
136         next:
137         for (RemoteRepository recessiveRepository : recessiveRepositories) {
138             RemoteRepository repository = recessiveRepository;
139             boolean mirrored = false;
140 
141             if (recessiveIsRaw) {
142                 RemoteRepository mirrorRepository = mirrorSelector.getMirror(recessiveRepository);
143 
144                 if (mirrorRepository != null) {
145                     logMirror(session, recessiveRepository, mirrorRepository);
146                     repository = mirrorRepository;
147                     mirrored = true;
148                 }
149             }
150 
151             String key = repositoryKeyFunction.apply(repository, null);
152 
153             for (ListIterator<RemoteRepository> it = result.listIterator(); it.hasNext(); ) {
154                 RemoteRepository dominantRepository = it.next();
155 
156                 if (key.equals(repositoryKeyFunction.apply(dominantRepository, null))) {
157                     if (!dominantRepository.getMirroredRepositories().isEmpty()
158                             && !repository.getMirroredRepositories().isEmpty()) {
159                         RemoteRepository mergedRepository = mergeMirrors(
160                                 session, repositoryKeyFunction, dominantRepository, repository, recessiveIsRaw);
161                         if (mergedRepository != dominantRepository) {
162                             it.set(mergedRepository);
163                         }
164                     }
165 
166                     continue next;
167                 }
168             }
169 
170             if (recessiveIsRaw) {
171                 RemoteRepository.Builder builder = null;
172                 Authentication auth = authSelector.getAuthentication(repository);
173                 if (auth != null) {
174                     if (!recessiveIsFromDescriptor || mirrored || authToDescriptorRepositories) {
175                         builder = new RemoteRepository.Builder(repository);
176                         builder.setAuthentication(auth);
177                     } else if (auth != repository.getAuthentication()) {
178                         logWarnOnce(
179                                 session,
180                                 "Not applying session authentication to repository {} ({}) declared by a remote"
181                                         + " artifact descriptor; set {}=true to restore the legacy behavior of"
182                                         + " matching credentials to such repositories by repository ID",
183                                 repository.getId(),
184                                 repository.getUrl(),
185                                 CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES);
186                     }
187                 }
188                 Proxy proxy = proxySelector.getProxy(repository);
189                 if (proxy != null) {
190                     if (builder == null) {
191                         builder = new RemoteRepository.Builder(repository);
192                     }
193                     builder.setProxy(proxy);
194                 }
195                 if (builder != null) {
196                     repository = builder.build();
197                 }
198             }
199 
200             result.add(repository);
201         }
202 
203         return result.stream()
204                 .map(r -> new RemoteRepository.Builder(r)
205                         .setIntent(RemoteRepository.Intent.RESOLUTION)
206                         .build())
207                 .collect(Collectors.toList());
208     }
209 
210     private void logMirror(RepositorySystemSession session, RemoteRepository original, RemoteRepository mirror) {
211         if (!LOGGER.isDebugEnabled()) {
212             return;
213         }
214         RepositoryCache cache = session.getCache();
215         if (cache != null) {
216             Object key = Keys.of(mirror.getId(), mirror.getUrl(), original.getId(), original.getUrl());
217             if (cache.get(session, key) != null) {
218                 return;
219             }
220             cache.put(session, key, Boolean.TRUE);
221         }
222         LOGGER.debug(
223                 "Using mirror {} ({}) for {} ({}).",
224                 mirror.getId(),
225                 mirror.getUrl(),
226                 original.getId(),
227                 original.getUrl());
228     }
229 
230     /**
231      * Flag indicating whether a repository declared by a remote artifact descriptor (POM) may weaken the checksum
232      * policy of the operator-defined mirror it is merged into. When disabled (the default), the effective checksum
233      * policy of a mirror never becomes weaker than what the mirror itself declares for the same nature: a recessive
234      * raw repository may still enable a nature or influence update policies, but a weaker checksum policy (e.g.
235      * {@code <checksumPolicy>ignore</checksumPolicy>} in a transitive POM) is not honored and a warning is logged
236      * instead. Enabling this restores the legacy weakest-wins merge, which let any POM in the dependency graph
237      * degrade or switch off checksum verification for downloads routed through the mirror.
238      *
239      * @since 2.0.23
240      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
241      * @configurationType {@link java.lang.Boolean}
242      * @configurationDefaultValue {@link #DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE}
243      */
244     public static final String CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE =
245             "aether.remoteRepositoryManager.rawRepositoryChecksumPolicyDowngrade";
246 
247     public static final boolean DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE = false;
248 
249     private static RepositoryPolicy clampChecksumPolicy(
250             RepositorySystemSession session, RepositoryPolicy merged, RepositoryPolicy floor, RemoteRepository rec) {
251         if (merged == null || floor == null) {
252             return merged;
253         }
254         if (checksumPolicyRank(merged.getChecksumPolicy()) < checksumPolicyRank(floor.getChecksumPolicy())) {
255             logWarnOnce(
256                     session,
257                     "Ignoring checksum policy '{}' contributed by repository {} ({}) declared by a remote artifact"
258                             + " descriptor: it would downgrade the checksum policy '{}' of the mirror serving it;"
259                             + " set {}=true to restore the legacy weakest-wins merge",
260                     merged.getChecksumPolicy(),
261                     rec.getId(),
262                     rec.getUrl(),
263                     floor.getChecksumPolicy(),
264                     CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE);
265             return new RepositoryPolicy(
266                     merged.isEnabled(),
267                     merged.getArtifactUpdatePolicy(),
268                     merged.getMetadataUpdatePolicy(),
269                     floor.getChecksumPolicy());
270         }
271         return merged;
272     }
273 
274     private static int checksumPolicyRank(String policy) {
275         if (policy == null) {
276             return -1;
277         }
278         switch (policy) {
279             case RepositoryPolicy.CHECKSUM_POLICY_FAIL:
280                 return 2;
281             case RepositoryPolicy.CHECKSUM_POLICY_WARN:
282                 return 1;
283             case RepositoryPolicy.CHECKSUM_POLICY_IGNORE:
284                 return 0;
285             default:
286                 // unknown (potentially attacker-supplied) values rank below any known policy, so they can never
287                 // displace an operator-configured one
288                 return -1;
289         }
290     }
291 
292     private RemoteRepository mergeMirrors(
293             RepositorySystemSession session,
294             RepositoryKeyFunction repositoryKeyFunction,
295             RemoteRepository dominant,
296             RemoteRepository recessive,
297             boolean recessiveIsRaw) {
298         boolean rawChecksumPolicyDowngrade = org.eclipse.aether.util.ConfigUtils.getBoolean(
299                 session, DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE, CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE);
300         RemoteRepository.Builder merged = null;
301         RepositoryPolicy releases = null, snapshots = null;
302 
303         next:
304         for (RemoteRepository rec : recessive.getMirroredRepositories()) {
305             String recKey = repositoryKeyFunction.apply(rec, null);
306 
307             for (RemoteRepository dom : dominant.getMirroredRepositories()) {
308                 if (recKey.equals(repositoryKeyFunction.apply(dom, null))) {
309                     continue next;
310                 }
311             }
312 
313             if (merged == null) {
314                 merged = new RemoteRepository.Builder(dominant);
315                 releases = dominant.getPolicy(false);
316                 snapshots = dominant.getPolicy(true);
317             }
318 
319             releases = merge(session, releases, rec.getPolicy(false), false);
320             snapshots = merge(session, snapshots, rec.getPolicy(true), false);
321 
322             if (recessiveIsRaw && !rawChecksumPolicyDowngrade) {
323                 // "recessive" originates from a remote artifact descriptor (POM): remotely supplied input must
324                 // never weaken the checksum policy the operator configured on the mirror itself
325                 releases = clampChecksumPolicy(session, releases, dominant.getPolicy(false), rec);
326                 snapshots = clampChecksumPolicy(session, snapshots, dominant.getPolicy(true), rec);
327             }
328 
329             merged.addMirroredRepository(rec);
330         }
331 
332         if (merged == null) {
333             return dominant;
334         }
335         return merged.setReleasePolicy(releases).setSnapshotPolicy(snapshots).build();
336     }
337 
338     /**
339      * Flag indicating whether the merge of a repository's release and snapshot policies (used when a single
340      * effective policy has to serve both natures, most notably for metadata of nature RELEASE_OR_SNAPSHOT such as
341      * {@code maven-metadata.xml} version lists) may pick the <em>weaker</em> of the two checksum policies, which was
342      * the legacy behavior. When disabled (the default), the stronger of the two checksum policies wins, so enabling
343      * snapshots with a lenient checksum policy no longer silently downgrades checksum enforcement below what the
344      * operator configured for releases (or vice versa). An explicit checksum policy set on the session (e.g. via
345      * {@code --strict-checksums}) takes precedence over either behavior, as before.
346      *
347      * @since 2.0.23
348      * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
349      * @configurationType {@link java.lang.Boolean}
350      * @configurationDefaultValue {@link #DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY}
351      */
352     public static final String CONFIG_PROP_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY =
353             "aether.remoteRepositoryManager.natureMergeWeakestChecksumPolicy";
354 
355     public static final boolean DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY = false;
356 
357     @Override
358     public RepositoryPolicy getPolicy(
359             RepositorySystemSession session, RemoteRepository repository, boolean releases, boolean snapshots) {
360         requireNonNull(session, "session cannot be null");
361         requireNonNull(repository, "repository cannot be null");
362         RepositoryPolicy policy1 = releases ? repository.getPolicy(false) : null;
363         RepositoryPolicy policy2 = snapshots ? repository.getPolicy(true) : null;
364         return merge(session, policy1, policy2, true);
365     }
366 
367     private RepositoryPolicy merge(
368             RepositorySystemSession session, RepositoryPolicy policy1, RepositoryPolicy policy2, boolean globalPolicy) {
369         RepositoryPolicy policy;
370 
371         if (policy2 == null) {
372             if (globalPolicy) {
373                 policy = merge(
374                         policy1,
375                         session.getArtifactUpdatePolicy(),
376                         session.getMetadataUpdatePolicy(),
377                         session.getChecksumPolicy());
378             } else {
379                 policy = policy1;
380             }
381         } else if (policy1 == null) {
382             if (globalPolicy) {
383                 policy = merge(
384                         policy2,
385                         session.getArtifactUpdatePolicy(),
386                         session.getMetadataUpdatePolicy(),
387                         session.getChecksumPolicy());
388             } else {
389                 policy = policy2;
390             }
391         } else if (!policy2.isEnabled()) {
392             if (globalPolicy) {
393                 policy = merge(
394                         policy1,
395                         session.getArtifactUpdatePolicy(),
396                         session.getMetadataUpdatePolicy(),
397                         session.getChecksumPolicy());
398             } else {
399                 policy = policy1;
400             }
401         } else if (!policy1.isEnabled()) {
402             if (globalPolicy) {
403                 policy = merge(
404                         policy2,
405                         session.getArtifactUpdatePolicy(),
406                         session.getMetadataUpdatePolicy(),
407                         session.getChecksumPolicy());
408             } else {
409                 policy = policy2;
410             }
411         } else {
412             String checksums = session.getChecksumPolicy();
413             //noinspection StatementWithEmptyBody
414             if (globalPolicy && checksums != null && !checksums.isEmpty()) {
415                 // use global override
416             } else if (globalPolicy
417                     && !org.eclipse.aether.util.ConfigUtils.getBoolean(
418                             session,
419                             DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY,
420                             CONFIG_PROP_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY)) {
421                 // merging the release and snapshot policies of a single repository into one effective policy
422                 // (e.g. for metadata of nature RELEASE_OR_SNAPSHOT): the result must not be weaker than what the
423                 // operator configured for either nature, so the stronger checksum policy wins
424                 checksums = strongerChecksumPolicy(policy1.getChecksumPolicy(), policy2.getChecksumPolicy());
425             } else {
426                 checksums = checksumPolicyProvider.getEffectiveChecksumPolicy(
427                         session, policy1.getChecksumPolicy(), policy2.getChecksumPolicy());
428             }
429 
430             String artifactUpdates = session.getArtifactUpdatePolicy();
431             //noinspection StatementWithEmptyBody
432             if (globalPolicy && artifactUpdates != null && !artifactUpdates.isEmpty()) {
433                 // use global override
434             } else {
435                 artifactUpdates = updatePolicyAnalyzer.getEffectiveUpdatePolicy(
436                         session, policy1.getArtifactUpdatePolicy(), policy2.getArtifactUpdatePolicy());
437             }
438             String metadataUpdates = session.getMetadataUpdatePolicy();
439             if (globalPolicy && metadataUpdates != null && !metadataUpdates.isEmpty()) {
440                 // use global override
441             } else {
442                 metadataUpdates = updatePolicyAnalyzer.getEffectiveUpdatePolicy(
443                         session, policy1.getMetadataUpdatePolicy(), policy2.getMetadataUpdatePolicy());
444             }
445 
446             policy = new RepositoryPolicy(true, artifactUpdates, metadataUpdates, checksums);
447         }
448 
449         return policy;
450     }
451 
452     private RepositoryPolicy merge(
453             RepositoryPolicy policy, String artifactUpdates, String metadataUpdates, String checksums) {
454         if (policy != null) {
455             if (artifactUpdates == null || artifactUpdates.isEmpty()) {
456                 artifactUpdates = policy.getArtifactUpdatePolicy();
457             }
458             if (metadataUpdates == null || metadataUpdates.isEmpty()) {
459                 metadataUpdates = policy.getMetadataUpdatePolicy();
460             }
461             if (checksums == null || checksums.isEmpty()) {
462                 checksums = policy.getChecksumPolicy();
463             }
464             if (!policy.getArtifactUpdatePolicy().equals(artifactUpdates)
465                     || !policy.getMetadataUpdatePolicy().equals(metadataUpdates)
466                     || !policy.getChecksumPolicy().equals(checksums)) {
467                 policy = new RepositoryPolicy(policy.isEnabled(), artifactUpdates, metadataUpdates, checksums);
468             }
469         }
470         return policy;
471     }
472 
473     private static String strongerChecksumPolicy(String policy1, String policy2) {
474         if (checksumPolicyStrength(policy2) > checksumPolicyStrength(policy1)) {
475             return policy2;
476         }
477         return policy1;
478     }
479 
480     private static int checksumPolicyStrength(String policy) {
481         if (policy == null) {
482             return -1;
483         }
484         switch (policy) {
485             case RepositoryPolicy.CHECKSUM_POLICY_FAIL:
486                 return 2;
487             case RepositoryPolicy.CHECKSUM_POLICY_WARN:
488                 return 1;
489             case RepositoryPolicy.CHECKSUM_POLICY_IGNORE:
490                 return 0;
491             default:
492                 // unknown values never win a strength comparison; they are rejected downstream when a
493                 // ChecksumPolicy instance is created for them
494                 return -1;
495         }
496     }
497 
498     @SuppressWarnings("unchecked")
499     private static void logWarnOnce(RepositorySystemSession session, String message, Object... args) {
500         if (!LOGGER.isWarnEnabled()) {
501             return;
502         }
503         Object[] keys = new Object[args.length + 1];
504         keys[0] = message;
505         System.arraycopy(args, 0, keys, 1, args.length);
506         Object key = Keys.of(keys);
507         ((Map<Object, Boolean>) session.getData()
508                         .computeIfAbsent(
509                                 Keys.of(DefaultRemoteRepositoryManager.class, "logWarnOnce"), ConcurrentHashMap::new))
510                 .computeIfAbsent(key, k -> {
511                     LOGGER.warn(message, args);
512                     return true;
513                 });
514     }
515 }