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.filter;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.net.URI;
026import java.net.URISyntaxException;
027import java.nio.file.Files;
028import java.nio.file.Path;
029import java.util.Collections;
030import java.util.List;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.ConcurrentMap;
033import java.util.concurrent.atomic.AtomicBoolean;
034import java.util.function.Supplier;
035
036import org.eclipse.aether.DefaultRepositorySystemSession;
037import org.eclipse.aether.Keys;
038import org.eclipse.aether.RepositorySystemSession;
039import org.eclipse.aether.artifact.Artifact;
040import org.eclipse.aether.impl.MetadataResolver;
041import org.eclipse.aether.impl.RemoteRepositoryManager;
042import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource;
043import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree;
044import org.eclipse.aether.metadata.DefaultMetadata;
045import org.eclipse.aether.metadata.Metadata;
046import org.eclipse.aether.repository.RemoteRepository;
047import org.eclipse.aether.resolution.MetadataRequest;
048import org.eclipse.aether.resolution.MetadataResult;
049import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
050import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
051import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
052import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
053import org.eclipse.aether.spi.connector.transport.PeekTask;
054import org.eclipse.aether.spi.connector.transport.Transporter;
055import org.eclipse.aether.spi.connector.transport.TransporterProvider;
056import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
057import org.eclipse.aether.transfer.NoRepositoryLayoutException;
058import org.eclipse.aether.util.ConfigUtils;
059
060import static java.util.Objects.requireNonNull;
061
062/**
063 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path
064 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in
065 * path with no corresponding prefix present in this file is filtered out.
066 * <p>
067 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines
068 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and
069 * MRMs publish these kind of files, they can be downloaded from corresponding URLs.
070 * <p>
071 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt".
072 * <p>
073 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not
074 * noticed.
075 * <p>
076 * Examples of published prefix files:
077 * <ul>
078 *     <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li>
079 *     <li>Apache Releases:
080 *     <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li>
081 * </ul>
082 *
083 * @since 1.9.0
084 */
085@Singleton
086@Named(PrefixesRemoteRepositoryFilterSource.NAME)
087public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport {
088    public static final String NAME = "prefixes";
089
090    static final String PREFIX_FILE_TYPE = ".meta/prefixes.txt";
091
092    /**
093     * Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using
094     * repository ID suffixes.
095     * <strong>Important:</strong> For this filter to take effect, configuration files must be available. Without
096     * configuration files, the enabled filter remains dormant and does not interfere with resolution.
097     * <strong>Configuration File Resolution:</strong>
098     * <ol>
099     * <li><strong>User-provided files:</strong> Checked first from directory specified by {@link #CONFIG_PROP_BASEDIR}
100     *     (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li>
101     * <li><strong>Auto-discovery:</strong> If not found, attempts to download from remote repository and cache locally</li>
102     * </ol>
103     * <strong>File Naming:</strong> {@code prefixes-$(repository.id).txt}
104     * <strong>Recommended Setup (Auto-Discovery with Override Capability):</strong>
105     * Start with auto-discovery, but prepare for project-specific overrides. Add to {@code .mvn/maven.config}:
106     * <pre>
107     * -Daether.remoteRepositoryFilter.prefixes=true
108     * -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/
109     * </pre>
110     * <strong>Initial setup:</strong> Don't provide any files - rely on auto-discovery as repositories are accessed.
111     * <strong>Override when needed:</strong> Create {@code prefixes-myrepoId.txt} files in {@code .mvn/rrf/} and
112     * commit to version control.
113     * <strong>Caching:</strong> Auto-discovered prefix files are cached in the local repository.
114     *
115     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
116     * @configurationType {@link java.lang.Boolean}
117     * @configurationRepoIdSuffix Yes
118     * @configurationDefaultValue {@link #DEFAULT_ENABLED}
119     */
120    public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME;
121
122    public static final boolean DEFAULT_ENABLED = true;
123
124    /**
125     * Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if {@code true}
126     * the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during
127     * discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while
128     * leaving configuration like {@link #CONFIG_PROP_ENABLED} still show the "real state".
129     *
130     * @since 2.0.14
131     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
132     * @configurationType {@link java.lang.Boolean}
133     * @configurationRepoIdSuffix Yes
134     * @configurationDefaultValue {@link #DEFAULT_SKIPPED}
135     */
136    public static final String CONFIG_PROP_SKIPPED =
137            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped";
138
139    public static final boolean DEFAULT_SKIPPED = false;
140
141    /**
142     * Determines what happens when the filter is enabled, but has no prefixes available for given remote repository
143     * to work with. When set to {@code true} (default), the filter allows all requests to proceed for given remote
144     * repository when no prefixes are available. When set to {@code false}, the filter blocks all requests toward
145     * given remote repository when no prefixes are available. This setting allows repoId suffix, hence, can
146     * determine "global" or "repository targeted" behaviors.
147     *
148     * @since 2.0.14
149     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
150     * @configurationType {@link java.lang.Boolean}
151     * @configurationRepoIdSuffix Yes
152     * @configurationDefaultValue {@link #DEFAULT_NO_INPUT_OUTCOME}
153     */
154    public static final String CONFIG_PROP_NO_INPUT_OUTCOME =
155            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".noInputOutcome";
156
157    public static final boolean DEFAULT_NO_INPUT_OUTCOME = true;
158
159    /**
160     * Configuration to allow Prefixes file resolution attempt from remote repository as "auto discovery". If this
161     * configuration set to {@code false} only user-provided prefixes will be used.
162     *
163     * @since 2.0.14
164     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
165     * @configurationType {@link java.lang.Boolean}
166     * @configurationRepoIdSuffix Yes
167     * @configurationDefaultValue {@link #DEFAULT_RESOLVE_PREFIX_FILES}
168     */
169    public static final String CONFIG_PROP_RESOLVE_PREFIX_FILES =
170            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".resolvePrefixFiles";
171
172    public static final boolean DEFAULT_RESOLVE_PREFIX_FILES = true;
173
174    /**
175     * Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to
176     * work <em>Maven should be aware</em> that given remote repository is mirror and is usually backed by MRM. Given
177     * multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
178     * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
179     *
180     * @since 2.0.14
181     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
182     * @configurationType {@link java.lang.Boolean}
183     * @configurationRepoIdSuffix Yes
184     * @configurationDefaultValue {@link #DEFAULT_USE_MIRRORED_REPOSITORIES}
185     */
186    public static final String CONFIG_PROP_USE_MIRRORED_REPOSITORIES =
187            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useMirroredRepositories";
188
189    public static final boolean DEFAULT_USE_MIRRORED_REPOSITORIES = false;
190
191    /**
192     * Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to
193     * work <em>Maven should be aware</em> that given remote repository is backed by repository manager.
194     * Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
195     * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
196     * <em>Note: as of today, nothing sets this on remote repositories, but is added for future.</em>
197     *
198     * @since 2.0.14
199     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
200     * @configurationType {@link java.lang.Boolean}
201     * @configurationRepoIdSuffix Yes
202     * @configurationDefaultValue {@link #DEFAULT_USE_REPOSITORY_MANAGERS}
203     */
204    public static final String CONFIG_PROP_USE_REPOSITORY_MANAGERS =
205            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useRepositoryManagers";
206
207    public static final boolean DEFAULT_USE_REPOSITORY_MANAGERS = false;
208
209    /**
210     * Configuration to verify the first denied path per remote repository when the effective prefixes were
211     * auto-discovered: the denied path existence is checked directly against the remote repository, and if the
212     * path exists, the auto-discovered prefixes file is provably wrong for that path (it denies content the
213     * repository actually serves); a warning is emitted and, by default, only that verified path is allowed while
214     * the prefixes file stays enforcing for all other paths (see {@link #CONFIG_PROP_VERIFY_DENIED_DROPS_TREE}
215     * for the legacy behavior of dropping the whole file). If the path does not exist, the prefixes file is
216     * consistent with reality for this witness and stays trusted; no further verification happens for given remote
217     * repository, keeping the extra cost bounded to at most one existence check per remote repository per session.
218     * <p>
219     * This protects builds from broken repository managers that "leak" a member repository prefixes file through
220     * a group/virtual repository, silently disabling the whole repository. User-provided prefix files are
221     * authoritative and are never verified. Verification is skipped in offline mode.
222     *
223     * @since 2.0.21
224     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
225     * @configurationType {@link java.lang.Boolean}
226     * @configurationRepoIdSuffix Yes
227     * @configurationDefaultValue {@link #DEFAULT_VERIFY_DENIED}
228     */
229    public static final String CONFIG_PROP_VERIFY_DENIED =
230            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".verifyDenied";
231
232    public static final boolean DEFAULT_VERIFY_DENIED = true;
233
234    /**
235     * Configuration to control what happens when {@link #CONFIG_PROP_VERIFY_DENIED} verification proves an
236     * auto-discovered prefixes file wrong (it denies a path the remote repository actually serves). When set to
237     * {@code false} (default), only the verified path is allowed and the auto-discovered prefixes file stays
238     * enforcing for all other paths: a single remotely-observed inconsistency does not disable the
239     * dependency-confusion protection this filter provides for the whole repository. When set to {@code true},
240     * the legacy behavior is restored: the whole auto-discovered prefixes file is dropped for the rest of the
241     * session and the filter behaves as if no input was available (see {@link #CONFIG_PROP_NO_INPUT_OUTCOME}),
242     * favoring availability over filtering. User-provided prefix files are authoritative and are never dropped,
243     * regardless of this setting.
244     *
245     * @since 2.0.23
246     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
247     * @configurationType {@link java.lang.Boolean}
248     * @configurationRepoIdSuffix Yes
249     * @configurationDefaultValue {@link #DEFAULT_VERIFY_DENIED_DROPS_TREE}
250     */
251    public static final String CONFIG_PROP_VERIFY_DENIED_DROPS_TREE =
252            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".verifyDeniedDropsTree";
253
254    public static final boolean DEFAULT_VERIFY_DENIED_DROPS_TREE = false;
255
256    /**
257     * The basedir where to store filter files. If path is relative, it is resolved from local repository root.
258     *
259     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
260     * @configurationType {@link java.lang.String}
261     * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR}
262     */
263    public static final String CONFIG_PROP_BASEDIR =
264            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".basedir";
265
266    public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters";
267
268    static final String PREFIXES_FILE_PREFIX = "prefixes-";
269
270    static final String PREFIXES_FILE_SUFFIX = ".txt";
271
272    private final Supplier<MetadataResolver> metadataResolver;
273
274    private final Supplier<RemoteRepositoryManager> remoteRepositoryManager;
275
276    private final RepositoryLayoutProvider repositoryLayoutProvider;
277
278    private final TransporterProvider transporterProvider;
279
280    @Inject
281    public PrefixesRemoteRepositoryFilterSource(
282            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
283            Supplier<MetadataResolver> metadataResolver,
284            Supplier<RemoteRepositoryManager> remoteRepositoryManager,
285            RepositoryLayoutProvider repositoryLayoutProvider,
286            TransporterProvider transporterProvider) {
287        super(repositoryKeyFunctionFactory);
288        this.metadataResolver = requireNonNull(metadataResolver);
289        this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager);
290        this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider);
291        this.transporterProvider = requireNonNull(transporterProvider);
292    }
293
294    private static final Object PREFIXES_KEY = Keys.of(PrefixesRemoteRepositoryFilterSource.class, "prefixes");
295
296    @SuppressWarnings("unchecked")
297    private ConcurrentMap<RemoteRepository, CachedPrefixes> prefixes(RepositorySystemSession session) {
298        return (ConcurrentMap<RemoteRepository, CachedPrefixes>)
299                session.getData().computeIfAbsent(PREFIXES_KEY, ConcurrentHashMap::new);
300    }
301
302    private static final Object LAYOUTS_KEY = Keys.of(PrefixesRemoteRepositoryFilterSource.class, "layouts");
303
304    @SuppressWarnings("unchecked")
305    private ConcurrentMap<RemoteRepository, RepositoryLayout> layouts(RepositorySystemSession session) {
306        return (ConcurrentMap<RemoteRepository, RepositoryLayout>)
307                session.getData().computeIfAbsent(LAYOUTS_KEY, ConcurrentHashMap::new);
308    }
309
310    @Override
311    protected boolean isEnabled(RepositorySystemSession session) {
312        return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED)
313                && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED);
314    }
315
316    private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) {
317        if (isEnabled(session)) {
318            return ConfigUtils.getBoolean(
319                            session,
320                            DEFAULT_ENABLED,
321                            CONFIG_PROP_ENABLED + "." + remoteRepository.getId(),
322                            CONFIG_PROP_ENABLED + ".*")
323                    && !ConfigUtils.getBoolean(
324                            session,
325                            DEFAULT_SKIPPED,
326                            CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(),
327                            CONFIG_PROP_SKIPPED + ".*");
328        }
329        return false;
330    }
331
332    @Override
333    public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
334        if (isEnabled(session)) {
335            return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false));
336        }
337        return null;
338    }
339
340    /**
341     * Caches layout instances for remote repository. In case of unknown layout it returns {@link #NOT_SUPPORTED}.
342     *
343     * @return the layout instance or {@link #NOT_SUPPORTED} if layout not supported.
344     */
345    private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) {
346        return layouts(session).computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> {
347            try {
348                return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository);
349            } catch (NoRepositoryLayoutException e) {
350                return NOT_SUPPORTED;
351            }
352        });
353    }
354
355    private CachedPrefixes cachePrefixes(
356            RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) {
357        return prefixes(session)
358                .computeIfAbsent(
359                        normalizeRemoteRepository(session, remoteRepository),
360                        r -> loadPrefixes(session, basedir, remoteRepository));
361    }
362
363    private static final PrefixTree DISABLED = new PrefixTree("disabled");
364    private static final PrefixTree ENABLED_NO_INPUT = new PrefixTree("enabled-no-input");
365    private static final PrefixTree BROKEN = new PrefixTree("broken");
366
367    /**
368     * The cached per remote repository prefixes state: the effective {@link PrefixTree}, whether it was
369     * auto-discovered (as only auto-discovered prefixes are subject to denied path verification, see
370     * {@link #CONFIG_PROP_VERIFY_DENIED}), whether verification happened already, and the denied path (if any)
371     * that verification proved the remote repository actually serves.
372     */
373    private static final class CachedPrefixes {
374        private static final CachedPrefixes DISABLED_PREFIXES = new CachedPrefixes(DISABLED, false);
375        private static final CachedPrefixes NO_INPUT_PREFIXES = new CachedPrefixes(ENABLED_NO_INPUT, false);
376
377        private volatile PrefixTree prefixTree;
378        private final boolean autoDiscovered;
379        private final AtomicBoolean verifyClaimed = new AtomicBoolean(false);
380        private volatile String verifiedServedPath;
381
382        private CachedPrefixes(PrefixTree prefixTree, boolean autoDiscovered) {
383            this.prefixTree = prefixTree;
384            this.autoDiscovered = autoDiscovered;
385        }
386
387        private PrefixTree prefixTree() {
388            return prefixTree;
389        }
390
391        private boolean autoDiscovered() {
392            return autoDiscovered;
393        }
394
395        private boolean claimVerification() {
396            return verifyClaimed.compareAndSet(false, true);
397        }
398
399        private void drop() {
400            this.prefixTree = BROKEN;
401        }
402
403        private void allowVerifiedServedPath(String path) {
404            this.verifiedServedPath = path;
405        }
406
407        private boolean isVerifiedServedPath(String path) {
408            String verified = this.verifiedServedPath;
409            return verified != null && verified.equals(path);
410        }
411    }
412
413    private CachedPrefixes loadPrefixes(
414            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
415        if (isRepositoryFilteringEnabled(session, remoteRepository)) {
416            String origin = "user-provided";
417            Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository);
418            if (filePath == null) {
419                if (!supportedResolvePrefixesForRemoteRepository(session, remoteRepository)) {
420                    origin = "unsupported";
421                } else {
422                    origin = "auto-discovered";
423                    filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository);
424                }
425            }
426            if (filePath != null) {
427                PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath);
428                if (prefixesSource.valid()) {
429                    logger.debug(
430                            "Loaded prefixes for remote repository {} from {} file '{}'",
431                            prefixesSource.origin().getId(),
432                            origin,
433                            prefixesSource.path());
434                    PrefixTree prefixTree = new PrefixTree("");
435                    int rules = prefixTree.loadNodes(prefixesSource.entries().stream());
436                    logger.info(
437                            "Loaded {} {} prefixes for remote repository {} ({})",
438                            rules,
439                            origin,
440                            prefixesSource.origin().getId(),
441                            prefixesSource.path().getFileName());
442                    return new CachedPrefixes(prefixTree, "auto-discovered".equals(origin));
443                } else {
444                    logger.info(
445                            "Rejected {} prefixes for remote repository {} ({}): {}",
446                            origin,
447                            prefixesSource.origin().getId(),
448                            prefixesSource.path().getFileName(),
449                            prefixesSource.message());
450                }
451            }
452            logger.debug("Prefix file for remote repository {} not available", remoteRepository);
453            return CachedPrefixes.NO_INPUT_PREFIXES;
454        }
455        logger.debug("Prefix file for remote repository {} disabled", remoteRepository);
456        return CachedPrefixes.DISABLED_PREFIXES;
457    }
458
459    private Path resolvePrefixesFromLocalConfiguration(
460            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
461        for (String key : repositoryKeys(session, remoteRepository)) {
462            Path filePath = baseDir.resolve(PREFIXES_FILE_PREFIX + key + PREFIXES_FILE_SUFFIX);
463            if (Files.isReadable(filePath)) {
464                return filePath;
465            }
466        }
467        return null;
468    }
469
470    private boolean supportedResolvePrefixesForRemoteRepository(
471            RepositorySystemSession session, RemoteRepository remoteRepository) {
472        if (!ConfigUtils.getBoolean(
473                session,
474                DEFAULT_RESOLVE_PREFIX_FILES,
475                CONFIG_PROP_RESOLVE_PREFIX_FILES + "." + remoteRepository.getId(),
476                CONFIG_PROP_RESOLVE_PREFIX_FILES)) {
477            return false;
478        }
479        if (remoteRepository.isRepositoryManager()) {
480            return ConfigUtils.getBoolean(
481                    session, DEFAULT_USE_REPOSITORY_MANAGERS, CONFIG_PROP_USE_REPOSITORY_MANAGERS);
482        } else {
483            return remoteRepository.getMirroredRepositories().isEmpty()
484                    || ConfigUtils.getBoolean(
485                            session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES);
486        }
487    }
488
489    private Path resolvePrefixesFromRemoteRepository(
490            RepositorySystemSession session, RemoteRepository remoteRepository) {
491        MetadataResolver mr = metadataResolver.get();
492        RemoteRepositoryManager rm = remoteRepositoryManager.get();
493        if (mr != null && rm != null) {
494            // retrieve prefix as metadata from repository
495            MetadataResult result = mr.resolveMetadata(
496                            new DefaultRepositorySystemSession(session)
497                                    .setTransferListener(null)
498                                    .setConfigProperty(CONFIG_PROP_SKIPPED, Boolean.TRUE.toString()),
499                            Collections.singleton(new MetadataRequest(
500                                            new DefaultMetadata(PREFIX_FILE_TYPE, Metadata.Nature.RELEASE_OR_SNAPSHOT))
501                                    .setRepository(remoteRepository)
502                                    .setDeleteLocalCopyIfMissing(true)
503                                    .setFavorLocalRepository(true)))
504                    .get(0);
505            if (result.isResolved()) {
506                return result.getMetadata().getPath();
507            } else {
508                return null;
509            }
510        }
511        return null;
512    }
513
514    private class PrefixesFilter implements RemoteRepositoryFilter {
515        private final RepositorySystemSession session;
516        private final Path basedir;
517
518        private PrefixesFilter(RepositorySystemSession session, Path basedir) {
519            this.session = session;
520            this.basedir = basedir;
521        }
522
523        @Override
524        public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
525            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
526            if (repositoryLayout == NOT_SUPPORTED) {
527                return result(true, NAME, "Unsupported layout: " + remoteRepository);
528            }
529            return acceptPrefix(
530                    remoteRepository,
531                    repositoryLayout.getLocation(artifact, false).getPath());
532        }
533
534        @Override
535        public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
536            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
537            if (repositoryLayout == NOT_SUPPORTED) {
538                return result(true, NAME, "Unsupported layout: " + remoteRepository);
539            }
540            return acceptPrefix(
541                    remoteRepository,
542                    repositoryLayout.getLocation(metadata, false).getPath());
543        }
544
545        private Result acceptPrefix(RemoteRepository repository, String path) {
546            CachedPrefixes cachedPrefixes = cachePrefixes(session, basedir, repository);
547            PrefixTree prefixTree = cachedPrefixes.prefixTree();
548            if (prefixTree == DISABLED) {
549                return result(true, NAME, "Disabled");
550            } else if (prefixTree == ENABLED_NO_INPUT) {
551                return noInputResult(repository, "No input available");
552            } else if (prefixTree == BROKEN) {
553                return noInputResult(repository, "Broken auto-discovered prefixes dropped");
554            }
555            boolean accepted = prefixTree.acceptedPath(path);
556            if (!accepted && cachedPrefixes.autoDiscovered() && isVerifyDeniedEnabled(repository)) {
557                // synchronized: only the first denial is verified; concurrent denials wait for the verdict
558                synchronized (cachedPrefixes) {
559                    if (cachedPrefixes.claimVerification() && remoteRepositoryServesPath(repository, path)) {
560                        if (isVerifyDeniedDropsTreeEnabled(repository)) {
561                            logger.warn(
562                                    "Remote repository {} serves a broken prefixes file: it denies path {} that the "
563                                            + "repository actually serves; ignoring auto-discovered prefixes for this "
564                                            + "repository (report this to the repository administrator)",
565                                    repository.getId(),
566                                    path);
567                            cachedPrefixes.drop();
568                        } else {
569                            logger.warn(
570                                    "Remote repository {} serves path {} that its auto-discovered prefixes file "
571                                            + "denies; the prefixes file appears stale. Allowing only this verified "
572                                            + "path; the prefixes filter stays enforcing for all other paths (set {} "
573                                            + "to true to instead drop the whole auto-discovered prefixes file; "
574                                            + "report this to the repository administrator)",
575                                    repository.getId(),
576                                    path,
577                                    CONFIG_PROP_VERIFY_DENIED_DROPS_TREE);
578                            cachedPrefixes.allowVerifiedServedPath(path);
579                        }
580                    }
581                }
582                if (cachedPrefixes.prefixTree() == BROKEN) {
583                    return noInputResult(repository, "Broken auto-discovered prefixes dropped");
584                }
585                if (cachedPrefixes.isVerifiedServedPath(path)) {
586                    return result(
587                            true,
588                            NAME,
589                            "Path " + path + " allowed from " + repository.getId()
590                                    + " (verified served despite stale auto-discovered prefixes)");
591                }
592            }
593            return result(
594                    accepted,
595                    NAME,
596                    accepted
597                            ? "Path " + path + " allowed from " + repository.getId()
598                            : "Path " + path + " NOT allowed from " + repository.getId());
599        }
600
601        private Result noInputResult(RemoteRepository repository, String reasoning) {
602            return result(
603                    ConfigUtils.getBoolean(
604                            session,
605                            DEFAULT_NO_INPUT_OUTCOME,
606                            CONFIG_PROP_NO_INPUT_OUTCOME + "." + repository.getId(),
607                            CONFIG_PROP_NO_INPUT_OUTCOME),
608                    NAME,
609                    reasoning);
610        }
611
612        private boolean isVerifyDeniedEnabled(RemoteRepository repository) {
613            return !session.isOffline()
614                    && ConfigUtils.getBoolean(
615                            session,
616                            DEFAULT_VERIFY_DENIED,
617                            CONFIG_PROP_VERIFY_DENIED + "." + repository.getId(),
618                            CONFIG_PROP_VERIFY_DENIED);
619        }
620
621        private boolean isVerifyDeniedDropsTreeEnabled(RemoteRepository repository) {
622            return ConfigUtils.getBoolean(
623                    session,
624                    DEFAULT_VERIFY_DENIED_DROPS_TREE,
625                    CONFIG_PROP_VERIFY_DENIED_DROPS_TREE + "." + repository.getId(),
626                    CONFIG_PROP_VERIFY_DENIED_DROPS_TREE);
627        }
628
629        /**
630         * Checks whether the remote repository actually serves given path, using a lightweight existence check
631         * (the transporter sits below the filtering connector, so no recursion can happen). Any failure (path
632         * not present, transport problem) yields {@code false}: the prefixes verdict is overridden (or, with
633         * {@link #CONFIG_PROP_VERIFY_DENIED_DROPS_TREE}, the whole file dropped) only when the remote repository
634         * provably serves the denied path.
635         */
636        private boolean remoteRepositoryServesPath(RemoteRepository repository, String path) {
637            try (Transporter transporter = transporterProvider.newTransporter(session, repository)) {
638                transporter.peek(new PeekTask(new URI(null, null, path, null)));
639                return true;
640            } catch (URISyntaxException e) {
641                logger.debug("Cannot construct URI for denied path {} of {}", path, repository, e);
642                return false;
643            } catch (Exception e) {
644                logger.debug("Verification of denied path {} against {} failed", path, repository, e);
645                return false;
646            }
647        }
648    }
649
650    private static final RepositoryLayout NOT_SUPPORTED = new RepositoryLayout() {
651        @Override
652        public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() {
653            throw new UnsupportedOperationException();
654        }
655
656        @Override
657        public boolean hasChecksums(Artifact artifact) {
658            throw new UnsupportedOperationException();
659        }
660
661        @Override
662        public URI getLocation(Artifact artifact, boolean upload) {
663            throw new UnsupportedOperationException();
664        }
665
666        @Override
667        public URI getLocation(Metadata metadata, boolean upload) {
668            throw new UnsupportedOperationException();
669        }
670
671        @Override
672        public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) {
673            throw new UnsupportedOperationException();
674        }
675
676        @Override
677        public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) {
678            throw new UnsupportedOperationException();
679        }
680    };
681}