001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.impl;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.util.ArrayList;
026import java.util.List;
027import java.util.ListIterator;
028import java.util.Map;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.stream.Collectors;
031
032import org.eclipse.aether.ConfigurationProperties;
033import org.eclipse.aether.Keys;
034import org.eclipse.aether.RepositoryCache;
035import org.eclipse.aether.RepositorySystemSession;
036import org.eclipse.aether.impl.RemoteRepositoryManager;
037import org.eclipse.aether.impl.UpdatePolicyAnalyzer;
038import org.eclipse.aether.repository.Authentication;
039import org.eclipse.aether.repository.AuthenticationSelector;
040import org.eclipse.aether.repository.MirrorSelector;
041import org.eclipse.aether.repository.Proxy;
042import org.eclipse.aether.repository.ProxySelector;
043import org.eclipse.aether.repository.RemoteRepository;
044import org.eclipse.aether.repository.RepositoryKeyFunction;
045import org.eclipse.aether.repository.RepositoryPolicy;
046import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
047import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
048import org.eclipse.aether.util.ConfigUtils;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052import static java.util.Objects.requireNonNull;
053
054/**
055 */
056@Singleton
057@Named
058public class DefaultRemoteRepositoryManager implements RemoteRepositoryManager {
059
060    private static final String CONFIG_PROPS_PREFIX =
061            ConfigurationProperties.PREFIX_AETHER + "remoteRepositoryManager.";
062
063    /**
064     * Flag indicating whether session authentication (i.e. credentials configured in {@code settings.xml}) may be
065     * applied, matched by plain repository ID, to repositories declared by remote artifact descriptors (POMs) that
066     * are merged into the effective repository list during dependency collection. When disabled (the default),
067     * session authentication is only applied to such a repository when an operator-defined mirror has been selected
068     * for it; if credentials would have matched a descriptor-declared repository, a warning naming the repository ID
069     * and URL is logged instead. Repositories supplied by the build itself (e.g. aggregated via
070     * {@code RepositorySystem#newResolutionRepositories}) are unaffected and keep receiving matching credentials.
071     * Enabling this restores the legacy behavior of applying matching session authentication to descriptor
072     * declared repositories regardless of their provenance.
073     *
074     * @since 2.0.23
075     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
076     * @configurationType {@link java.lang.Boolean}
077     * @configurationDefaultValue {@link #DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES}
078     */
079    public static final String CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES =
080            CONFIG_PROPS_PREFIX + "authToDescriptorRepositories";
081
082    public static final boolean DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES = false;
083
084    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRemoteRepositoryManager.class);
085
086    private final UpdatePolicyAnalyzer updatePolicyAnalyzer;
087
088    private final ChecksumPolicyProvider checksumPolicyProvider;
089
090    private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory;
091
092    @Inject
093    public DefaultRemoteRepositoryManager(
094            UpdatePolicyAnalyzer updatePolicyAnalyzer,
095            ChecksumPolicyProvider checksumPolicyProvider,
096            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) {
097        this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
098        this.checksumPolicyProvider = requireNonNull(checksumPolicyProvider, "checksum policy provider cannot be null");
099        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}