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}