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