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.nio.file.Files;
027import java.nio.file.Path;
028import java.util.Collections;
029import java.util.List;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.concurrent.ConcurrentMap;
032import java.util.function.Supplier;
033
034import org.eclipse.aether.DefaultRepositorySystemSession;
035import org.eclipse.aether.RepositorySystemSession;
036import org.eclipse.aether.artifact.Artifact;
037import org.eclipse.aether.impl.MetadataResolver;
038import org.eclipse.aether.impl.RemoteRepositoryManager;
039import org.eclipse.aether.internal.impl.filter.prefixes.PrefixesSource;
040import org.eclipse.aether.internal.impl.filter.ruletree.PrefixTree;
041import org.eclipse.aether.metadata.DefaultMetadata;
042import org.eclipse.aether.metadata.Metadata;
043import org.eclipse.aether.repository.RemoteRepository;
044import org.eclipse.aether.resolution.MetadataRequest;
045import org.eclipse.aether.resolution.MetadataResult;
046import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
047import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
048import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
049import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
050import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory;
051import org.eclipse.aether.transfer.NoRepositoryLayoutException;
052import org.eclipse.aether.util.ConfigUtils;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056import static java.util.Objects.requireNonNull;
057
058/**
059 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path
060 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in
061 * path with no corresponding prefix present in this file is filtered out.
062 * <p>
063 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines
064 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and
065 * MRMs publish these kind of files, they can be downloaded from corresponding URLs.
066 * <p>
067 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt".
068 * <p>
069 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not
070 * noticed.
071 * <p>
072 * Examples of published prefix files:
073 * <ul>
074 *     <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li>
075 *     <li>Apache Releases:
076 *     <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li>
077 * </ul>
078 *
079 * @since 1.9.0
080 */
081@Singleton
082@Named(PrefixesRemoteRepositoryFilterSource.NAME)
083public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport {
084    public static final String NAME = "prefixes";
085
086    static final String PREFIX_FILE_TYPE = ".meta/prefixes.txt";
087
088    /**
089     * Configuration to enable the Prefixes filter (enabled by default). Can be fine-tuned per repository using
090     * repository ID suffixes.
091     * <strong>Important:</strong> For this filter to take effect, configuration files must be available. Without
092     * configuration files, the enabled filter remains dormant and does not interfere with resolution.
093     * <strong>Configuration File Resolution:</strong>
094     * <ol>
095     * <li><strong>User-provided files:</strong> Checked first from directory specified by {@link #CONFIG_PROP_BASEDIR}
096     *     (defaults to {@code $LOCAL_REPO/.remoteRepositoryFilters})</li>
097     * <li><strong>Auto-discovery:</strong> If not found, attempts to download from remote repository and cache locally</li>
098     * </ol>
099     * <strong>File Naming:</strong> {@code prefixes-$(repository.id).txt}
100     * <strong>Recommended Setup (Auto-Discovery with Override Capability):</strong>
101     * Start with auto-discovery, but prepare for project-specific overrides. Add to {@code .mvn/maven.config}:
102     * <pre>
103     * -Daether.remoteRepositoryFilter.prefixes=true
104     * -Daether.remoteRepositoryFilter.prefixes.basedir=${session.rootDirectory}/.mvn/rrf/
105     * </pre>
106     * <strong>Initial setup:</strong> Don't provide any files - rely on auto-discovery as repositories are accessed.
107     * <strong>Override when needed:</strong> Create {@code prefixes-myrepoId.txt} files in {@code .mvn/rrf/} and
108     * commit to version control.
109     * <strong>Caching:</strong> Auto-discovered prefix files are cached in the local repository.
110     *
111     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
112     * @configurationType {@link java.lang.Boolean}
113     * @configurationRepoIdSuffix Yes
114     * @configurationDefaultValue {@link #DEFAULT_ENABLED}
115     */
116    public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME;
117
118    public static final boolean DEFAULT_ENABLED = true;
119
120    /**
121     * Configuration to skip the Prefixes filter for given request. This configuration is evaluated and if {@code true}
122     * the prefixes remote filter will not kick in. Main use case is by filter itself, to prevent recursion during
123     * discovery of remote prefixes file, but this also allows other components to control prefix filter discovery, while
124     * leaving configuration like {@link #CONFIG_PROP_ENABLED} still show the "real state".
125     *
126     * @since 2.0.14
127     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
128     * @configurationType {@link java.lang.Boolean}
129     * @configurationRepoIdSuffix Yes
130     * @configurationDefaultValue {@link #DEFAULT_SKIPPED}
131     */
132    public static final String CONFIG_PROP_SKIPPED =
133            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".skipped";
134
135    public static final boolean DEFAULT_SKIPPED = false;
136
137    /**
138     * Determines what happens when the filter is enabled, but has no prefixes available for given remote repository
139     * to work with. When set to {@code true} (default), the filter allows all requests to proceed for given remote
140     * repository when no prefixes are available. When set to {@code false}, the filter blocks all requests toward
141     * given remote repository when no prefixes are available. This setting allows repoId suffix, hence, can
142     * determine "global" or "repository targeted" behaviors.
143     *
144     * @since 2.0.14
145     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
146     * @configurationType {@link java.lang.Boolean}
147     * @configurationRepoIdSuffix Yes
148     * @configurationDefaultValue {@link #DEFAULT_NO_INPUT_OUTCOME}
149     */
150    public static final String CONFIG_PROP_NO_INPUT_OUTCOME =
151            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".noInputOutcome";
152
153    public static final boolean DEFAULT_NO_INPUT_OUTCOME = true;
154
155    /**
156     * Configuration to allow Prefixes file resolution attempt from remote repository as "auto discovery". If this
157     * configuration set to {@code false} only user-provided prefixes will be used.
158     *
159     * @since 2.0.14
160     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
161     * @configurationType {@link java.lang.Boolean}
162     * @configurationRepoIdSuffix Yes
163     * @configurationDefaultValue {@link #DEFAULT_RESOLVE_PREFIX_FILES}
164     */
165    public static final String CONFIG_PROP_RESOLVE_PREFIX_FILES =
166            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".resolvePrefixFiles";
167
168    public static final boolean DEFAULT_RESOLVE_PREFIX_FILES = true;
169
170    /**
171     * Configuration to allow Prefixes filter to auto-discover prefixes from mirrored repositories as well. For this to
172     * work <em>Maven should be aware</em> that given remote repository is mirror and is usually backed by MRM. Given
173     * multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
174     * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
175     *
176     * @since 2.0.14
177     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
178     * @configurationType {@link java.lang.Boolean}
179     * @configurationRepoIdSuffix Yes
180     * @configurationDefaultValue {@link #DEFAULT_USE_MIRRORED_REPOSITORIES}
181     */
182    public static final String CONFIG_PROP_USE_MIRRORED_REPOSITORIES =
183            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useMirroredRepositories";
184
185    public static final boolean DEFAULT_USE_MIRRORED_REPOSITORIES = false;
186
187    /**
188     * Configuration to allow Prefixes filter to auto-discover prefixes from repository managers as well. For this to
189     * work <em>Maven should be aware</em> that given remote repository is backed by repository manager.
190     * Given multiple MRM implementations messes up prefixes file, is better to just skip these. In other case, one may use
191     * {@link #CONFIG_PROP_ENABLED} with repository ID suffix.
192     * <em>Note: as of today, nothing sets this on remote repositories, but is added for future.</em>
193     *
194     * @since 2.0.14
195     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
196     * @configurationType {@link java.lang.Boolean}
197     * @configurationRepoIdSuffix Yes
198     * @configurationDefaultValue {@link #DEFAULT_USE_REPOSITORY_MANAGERS}
199     */
200    public static final String CONFIG_PROP_USE_REPOSITORY_MANAGERS =
201            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".useRepositoryManagers";
202
203    public static final boolean DEFAULT_USE_REPOSITORY_MANAGERS = false;
204
205    /**
206     * The basedir where to store filter files. If path is relative, it is resolved from local repository root.
207     *
208     * @configurationSource {@link RepositorySystemSession#getConfigProperties()}
209     * @configurationType {@link java.lang.String}
210     * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR}
211     */
212    public static final String CONFIG_PROP_BASEDIR =
213            RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + ".basedir";
214
215    public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters";
216
217    static final String PREFIXES_FILE_PREFIX = "prefixes-";
218
219    static final String PREFIXES_FILE_SUFFIX = ".txt";
220
221    private final Logger logger = LoggerFactory.getLogger(PrefixesRemoteRepositoryFilterSource.class);
222
223    private final Supplier<MetadataResolver> metadataResolver;
224
225    private final Supplier<RemoteRepositoryManager> remoteRepositoryManager;
226
227    private final RepositoryLayoutProvider repositoryLayoutProvider;
228
229    @Inject
230    public PrefixesRemoteRepositoryFilterSource(
231            RepositoryKeyFunctionFactory repositoryKeyFunctionFactory,
232            Supplier<MetadataResolver> metadataResolver,
233            Supplier<RemoteRepositoryManager> remoteRepositoryManager,
234            RepositoryLayoutProvider repositoryLayoutProvider) {
235        super(repositoryKeyFunctionFactory);
236        this.metadataResolver = requireNonNull(metadataResolver);
237        this.remoteRepositoryManager = requireNonNull(remoteRepositoryManager);
238        this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider);
239    }
240
241    @SuppressWarnings("unchecked")
242    private ConcurrentMap<RemoteRepository, PrefixTree> prefixes(RepositorySystemSession session) {
243        return (ConcurrentMap<RemoteRepository, PrefixTree>)
244                session.getData().computeIfAbsent(getClass().getName() + ".prefixes", ConcurrentHashMap::new);
245    }
246
247    @SuppressWarnings("unchecked")
248    private ConcurrentMap<RemoteRepository, RepositoryLayout> layouts(RepositorySystemSession session) {
249        return (ConcurrentMap<RemoteRepository, RepositoryLayout>)
250                session.getData().computeIfAbsent(getClass().getName() + ".layouts", ConcurrentHashMap::new);
251    }
252
253    @Override
254    protected boolean isEnabled(RepositorySystemSession session) {
255        return ConfigUtils.getBoolean(session, DEFAULT_ENABLED, CONFIG_PROP_ENABLED)
256                && !ConfigUtils.getBoolean(session, DEFAULT_SKIPPED, CONFIG_PROP_SKIPPED);
257    }
258
259    private boolean isRepositoryFilteringEnabled(RepositorySystemSession session, RemoteRepository remoteRepository) {
260        if (isEnabled(session)) {
261            return ConfigUtils.getBoolean(
262                            session,
263                            DEFAULT_ENABLED,
264                            CONFIG_PROP_ENABLED + "." + remoteRepository.getId(),
265                            CONFIG_PROP_ENABLED + ".*")
266                    && !ConfigUtils.getBoolean(
267                            session,
268                            DEFAULT_SKIPPED,
269                            CONFIG_PROP_SKIPPED + "." + remoteRepository.getId(),
270                            CONFIG_PROP_SKIPPED + ".*");
271        }
272        return false;
273    }
274
275    @Override
276    public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) {
277        if (isEnabled(session)) {
278            return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false));
279        }
280        return null;
281    }
282
283    /**
284     * Caches layout instances for remote repository. In case of unknown layout it returns {@link #NOT_SUPPORTED}.
285     *
286     * @return the layout instance or {@link #NOT_SUPPORTED} if layout not supported.
287     */
288    private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) {
289        return layouts(session).computeIfAbsent(normalizeRemoteRepository(session, remoteRepository), r -> {
290            try {
291                return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository);
292            } catch (NoRepositoryLayoutException e) {
293                return NOT_SUPPORTED;
294            }
295        });
296    }
297
298    private PrefixTree cachePrefixTree(
299            RepositorySystemSession session, Path basedir, RemoteRepository remoteRepository) {
300        return prefixes(session)
301                .computeIfAbsent(
302                        normalizeRemoteRepository(session, remoteRepository),
303                        r -> loadPrefixTree(session, basedir, remoteRepository));
304    }
305
306    private static final PrefixTree DISABLED = new PrefixTree("disabled");
307    private static final PrefixTree ENABLED_NO_INPUT = new PrefixTree("enabled-no-input");
308
309    private PrefixTree loadPrefixTree(
310            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
311        if (isRepositoryFilteringEnabled(session, remoteRepository)) {
312            String origin = "user-provided";
313            Path filePath = resolvePrefixesFromLocalConfiguration(session, baseDir, remoteRepository);
314            if (filePath == null) {
315                if (!supportedResolvePrefixesForRemoteRepository(session, remoteRepository)) {
316                    origin = "unsupported";
317                } else {
318                    origin = "auto-discovered";
319                    filePath = resolvePrefixesFromRemoteRepository(session, remoteRepository);
320                }
321            }
322            if (filePath != null) {
323                PrefixesSource prefixesSource = PrefixesSource.of(remoteRepository, filePath);
324                if (prefixesSource.valid()) {
325                    logger.debug(
326                            "Loaded prefixes for remote repository {} from {} file '{}'",
327                            prefixesSource.origin().getId(),
328                            origin,
329                            prefixesSource.path());
330                    PrefixTree prefixTree = new PrefixTree("");
331                    int rules = prefixTree.loadNodes(prefixesSource.entries().stream());
332                    logger.info(
333                            "Loaded {} {} prefixes for remote repository {} ({})",
334                            rules,
335                            origin,
336                            prefixesSource.origin().getId(),
337                            prefixesSource.path().getFileName());
338                    return prefixTree;
339                } else {
340                    logger.info(
341                            "Rejected {} prefixes for remote repository {} ({}): {}",
342                            origin,
343                            prefixesSource.origin().getId(),
344                            prefixesSource.path().getFileName(),
345                            prefixesSource.message());
346                }
347            }
348            logger.debug("Prefix file for remote repository {} not available", remoteRepository);
349            return ENABLED_NO_INPUT;
350        }
351        logger.debug("Prefix file for remote repository {} disabled", remoteRepository);
352        return DISABLED;
353    }
354
355    private Path resolvePrefixesFromLocalConfiguration(
356            RepositorySystemSession session, Path baseDir, RemoteRepository remoteRepository) {
357        Path filePath =
358                baseDir.resolve(PREFIXES_FILE_PREFIX + repositoryKey(session, remoteRepository) + PREFIXES_FILE_SUFFIX);
359        if (Files.isReadable(filePath)) {
360            return filePath;
361        } else {
362            return null;
363        }
364    }
365
366    private boolean supportedResolvePrefixesForRemoteRepository(
367            RepositorySystemSession session, RemoteRepository remoteRepository) {
368        if (!ConfigUtils.getBoolean(
369                session,
370                DEFAULT_RESOLVE_PREFIX_FILES,
371                CONFIG_PROP_RESOLVE_PREFIX_FILES + "." + remoteRepository.getId(),
372                CONFIG_PROP_RESOLVE_PREFIX_FILES)) {
373            return false;
374        }
375        if (remoteRepository.isRepositoryManager()) {
376            return ConfigUtils.getBoolean(
377                    session, DEFAULT_USE_REPOSITORY_MANAGERS, CONFIG_PROP_USE_REPOSITORY_MANAGERS);
378        } else {
379            return remoteRepository.getMirroredRepositories().isEmpty()
380                    || ConfigUtils.getBoolean(
381                            session, DEFAULT_USE_MIRRORED_REPOSITORIES, CONFIG_PROP_USE_MIRRORED_REPOSITORIES);
382        }
383    }
384
385    private Path resolvePrefixesFromRemoteRepository(
386            RepositorySystemSession session, RemoteRepository remoteRepository) {
387        MetadataResolver mr = metadataResolver.get();
388        RemoteRepositoryManager rm = remoteRepositoryManager.get();
389        if (mr != null && rm != null) {
390            // create "prepared" (auth, proxy and mirror equipped repo)
391            RemoteRepository prepared = rm.aggregateRepositories(
392                            session, Collections.emptyList(), Collections.singletonList(remoteRepository), true)
393                    .get(0);
394            // retrieve prefix as metadata from repository
395            MetadataResult result = mr.resolveMetadata(
396                            new DefaultRepositorySystemSession(session)
397                                    .setTransferListener(null)
398                                    .setConfigProperty(CONFIG_PROP_SKIPPED, Boolean.TRUE.toString()),
399                            Collections.singleton(new MetadataRequest(
400                                            new DefaultMetadata(PREFIX_FILE_TYPE, Metadata.Nature.RELEASE_OR_SNAPSHOT))
401                                    .setRepository(prepared)
402                                    .setDeleteLocalCopyIfMissing(true)
403                                    .setFavorLocalRepository(true)))
404                    .get(0);
405            if (result.isResolved()) {
406                return result.getMetadata().getPath();
407            } else {
408                return null;
409            }
410        }
411        return null;
412    }
413
414    private class PrefixesFilter implements RemoteRepositoryFilter {
415        private final RepositorySystemSession session;
416        private final Path basedir;
417
418        private PrefixesFilter(RepositorySystemSession session, Path basedir) {
419            this.session = session;
420            this.basedir = basedir;
421        }
422
423        @Override
424        public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) {
425            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
426            if (repositoryLayout == NOT_SUPPORTED) {
427                return result(true, NAME, "Unsupported layout: " + remoteRepository);
428            }
429            return acceptPrefix(
430                    remoteRepository,
431                    repositoryLayout.getLocation(artifact, false).getPath());
432        }
433
434        @Override
435        public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) {
436            RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository);
437            if (repositoryLayout == NOT_SUPPORTED) {
438                return result(true, NAME, "Unsupported layout: " + remoteRepository);
439            }
440            return acceptPrefix(
441                    remoteRepository,
442                    repositoryLayout.getLocation(metadata, false).getPath());
443        }
444
445        private Result acceptPrefix(RemoteRepository repository, String path) {
446            PrefixTree prefixTree = cachePrefixTree(session, basedir, repository);
447            if (prefixTree == DISABLED) {
448                return result(true, NAME, "Disabled");
449            } else if (prefixTree == ENABLED_NO_INPUT) {
450                return result(
451                        ConfigUtils.getBoolean(
452                                session,
453                                DEFAULT_NO_INPUT_OUTCOME,
454                                CONFIG_PROP_NO_INPUT_OUTCOME + "." + repository.getId(),
455                                CONFIG_PROP_NO_INPUT_OUTCOME),
456                        NAME,
457                        "No input available");
458            }
459            boolean accepted = prefixTree.acceptedPath(path);
460            return result(
461                    accepted,
462                    NAME,
463                    accepted
464                            ? "Path " + path + " allowed from " + repository.getId()
465                            : "Path " + path + " NOT allowed from " + repository.getId());
466        }
467    }
468
469    private static final RepositoryLayout NOT_SUPPORTED = new RepositoryLayout() {
470        @Override
471        public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() {
472            throw new UnsupportedOperationException();
473        }
474
475        @Override
476        public boolean hasChecksums(Artifact artifact) {
477            throw new UnsupportedOperationException();
478        }
479
480        @Override
481        public URI getLocation(Artifact artifact, boolean upload) {
482            throw new UnsupportedOperationException();
483        }
484
485        @Override
486        public URI getLocation(Metadata metadata, boolean upload) {
487            throw new UnsupportedOperationException();
488        }
489
490        @Override
491        public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) {
492            throw new UnsupportedOperationException();
493        }
494
495        @Override
496        public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) {
497            throw new UnsupportedOperationException();
498        }
499    };
500}