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