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}