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; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023import javax.inject.Singleton; 024 025import java.util.ArrayList; 026import java.util.List; 027import java.util.ListIterator; 028import java.util.Map; 029import java.util.concurrent.ConcurrentHashMap; 030import java.util.stream.Collectors; 031 032import org.eclipse.aether.ConfigurationProperties; 033import org.eclipse.aether.Keys; 034import org.eclipse.aether.RepositoryCache; 035import org.eclipse.aether.RepositorySystemSession; 036import org.eclipse.aether.impl.RemoteRepositoryManager; 037import org.eclipse.aether.impl.UpdatePolicyAnalyzer; 038import org.eclipse.aether.repository.Authentication; 039import org.eclipse.aether.repository.AuthenticationSelector; 040import org.eclipse.aether.repository.MirrorSelector; 041import org.eclipse.aether.repository.Proxy; 042import org.eclipse.aether.repository.ProxySelector; 043import org.eclipse.aether.repository.RemoteRepository; 044import org.eclipse.aether.repository.RepositoryKeyFunction; 045import org.eclipse.aether.repository.RepositoryPolicy; 046import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider; 047import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory; 048import org.eclipse.aether.util.ConfigUtils; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052import static java.util.Objects.requireNonNull; 053 054/** 055 */ 056@Singleton 057@Named 058public class DefaultRemoteRepositoryManager implements RemoteRepositoryManager { 059 060 private static final String CONFIG_PROPS_PREFIX = 061 ConfigurationProperties.PREFIX_AETHER + "remoteRepositoryManager."; 062 063 /** 064 * Flag indicating whether session authentication (i.e. credentials configured in {@code settings.xml}) may be 065 * applied, matched by plain repository ID, to repositories declared by remote artifact descriptors (POMs) that 066 * are merged into the effective repository list during dependency collection. When disabled (the default), 067 * session authentication is only applied to such a repository when an operator-defined mirror has been selected 068 * for it; if credentials would have matched a descriptor-declared repository, a warning naming the repository ID 069 * and URL is logged instead. Repositories supplied by the build itself (e.g. aggregated via 070 * {@code RepositorySystem#newResolutionRepositories}) are unaffected and keep receiving matching credentials. 071 * Enabling this restores the legacy behavior of applying matching session authentication to descriptor 072 * declared repositories regardless of their provenance. 073 * 074 * @since 2.0.23 075 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 076 * @configurationType {@link java.lang.Boolean} 077 * @configurationDefaultValue {@link #DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES} 078 */ 079 public static final String CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES = 080 CONFIG_PROPS_PREFIX + "authToDescriptorRepositories"; 081 082 public static final boolean DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES = false; 083 084 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRemoteRepositoryManager.class); 085 086 private final UpdatePolicyAnalyzer updatePolicyAnalyzer; 087 088 private final ChecksumPolicyProvider checksumPolicyProvider; 089 090 private final RepositoryKeyFunctionFactory repositoryKeyFunctionFactory; 091 092 @Inject 093 public DefaultRemoteRepositoryManager( 094 UpdatePolicyAnalyzer updatePolicyAnalyzer, 095 ChecksumPolicyProvider checksumPolicyProvider, 096 RepositoryKeyFunctionFactory repositoryKeyFunctionFactory) { 097 this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null"); 098 this.checksumPolicyProvider = requireNonNull(checksumPolicyProvider, "checksum policy provider cannot be null"); 099 this.repositoryKeyFunctionFactory = 100 requireNonNull(repositoryKeyFunctionFactory, "repository key function factory cannot be null"); 101 } 102 103 @Override 104 public List<RemoteRepository> aggregateRepositories( 105 RepositorySystemSession session, 106 List<RemoteRepository> dominantRepositories, 107 List<RemoteRepository> recessiveRepositories, 108 boolean recessiveIsRaw) { 109 return aggregateRepositories(session, dominantRepositories, recessiveRepositories, recessiveIsRaw, false); 110 } 111 112 @Override 113 public List<RemoteRepository> aggregateRepositories( 114 RepositorySystemSession session, 115 List<RemoteRepository> dominantRepositories, 116 List<RemoteRepository> recessiveRepositories, 117 boolean recessiveIsRaw, 118 boolean recessiveIsFromDescriptor) { 119 requireNonNull(session, "session cannot be null"); 120 requireNonNull(dominantRepositories, "dominantRepositories cannot be null"); 121 requireNonNull(recessiveRepositories, "recessiveRepositories cannot be null"); 122 if (recessiveRepositories.isEmpty()) { 123 return dominantRepositories; 124 } 125 126 RepositoryKeyFunction repositoryKeyFunction = repositoryKeyFunctionFactory.systemRepositoryKeyFunction(session); 127 MirrorSelector mirrorSelector = session.getMirrorSelector(); 128 AuthenticationSelector authSelector = session.getAuthenticationSelector(); 129 ProxySelector proxySelector = session.getProxySelector(); 130 131 boolean authToDescriptorRepositories = ConfigUtils.getBoolean( 132 session, DEFAULT_AUTH_TO_DESCRIPTOR_REPOSITORIES, CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES); 133 134 List<RemoteRepository> result = new ArrayList<>(dominantRepositories); 135 136 next: 137 for (RemoteRepository recessiveRepository : recessiveRepositories) { 138 RemoteRepository repository = recessiveRepository; 139 boolean mirrored = false; 140 141 if (recessiveIsRaw) { 142 RemoteRepository mirrorRepository = mirrorSelector.getMirror(recessiveRepository); 143 144 if (mirrorRepository != null) { 145 logMirror(session, recessiveRepository, mirrorRepository); 146 repository = mirrorRepository; 147 mirrored = true; 148 } 149 } 150 151 String key = repositoryKeyFunction.apply(repository, null); 152 153 for (ListIterator<RemoteRepository> it = result.listIterator(); it.hasNext(); ) { 154 RemoteRepository dominantRepository = it.next(); 155 156 if (key.equals(repositoryKeyFunction.apply(dominantRepository, null))) { 157 if (!dominantRepository.getMirroredRepositories().isEmpty() 158 && !repository.getMirroredRepositories().isEmpty()) { 159 RemoteRepository mergedRepository = mergeMirrors( 160 session, repositoryKeyFunction, dominantRepository, repository, recessiveIsRaw); 161 if (mergedRepository != dominantRepository) { 162 it.set(mergedRepository); 163 } 164 } 165 166 continue next; 167 } 168 } 169 170 if (recessiveIsRaw) { 171 RemoteRepository.Builder builder = null; 172 Authentication auth = authSelector.getAuthentication(repository); 173 if (auth != null) { 174 if (!recessiveIsFromDescriptor || mirrored || authToDescriptorRepositories) { 175 builder = new RemoteRepository.Builder(repository); 176 builder.setAuthentication(auth); 177 } else if (auth != repository.getAuthentication()) { 178 logWarnOnce( 179 session, 180 "Not applying session authentication to repository {} ({}) declared by a remote" 181 + " artifact descriptor; set {}=true to restore the legacy behavior of" 182 + " matching credentials to such repositories by repository ID", 183 repository.getId(), 184 repository.getUrl(), 185 CONFIG_PROP_AUTH_TO_DESCRIPTOR_REPOSITORIES); 186 } 187 } 188 Proxy proxy = proxySelector.getProxy(repository); 189 if (proxy != null) { 190 if (builder == null) { 191 builder = new RemoteRepository.Builder(repository); 192 } 193 builder.setProxy(proxy); 194 } 195 if (builder != null) { 196 repository = builder.build(); 197 } 198 } 199 200 result.add(repository); 201 } 202 203 return result.stream() 204 .map(r -> new RemoteRepository.Builder(r) 205 .setIntent(RemoteRepository.Intent.RESOLUTION) 206 .build()) 207 .collect(Collectors.toList()); 208 } 209 210 private void logMirror(RepositorySystemSession session, RemoteRepository original, RemoteRepository mirror) { 211 if (!LOGGER.isDebugEnabled()) { 212 return; 213 } 214 RepositoryCache cache = session.getCache(); 215 if (cache != null) { 216 Object key = Keys.of(mirror.getId(), mirror.getUrl(), original.getId(), original.getUrl()); 217 if (cache.get(session, key) != null) { 218 return; 219 } 220 cache.put(session, key, Boolean.TRUE); 221 } 222 LOGGER.debug( 223 "Using mirror {} ({}) for {} ({}).", 224 mirror.getId(), 225 mirror.getUrl(), 226 original.getId(), 227 original.getUrl()); 228 } 229 230 /** 231 * Flag indicating whether a repository declared by a remote artifact descriptor (POM) may weaken the checksum 232 * policy of the operator-defined mirror it is merged into. When disabled (the default), the effective checksum 233 * policy of a mirror never becomes weaker than what the mirror itself declares for the same nature: a recessive 234 * raw repository may still enable a nature or influence update policies, but a weaker checksum policy (e.g. 235 * {@code <checksumPolicy>ignore</checksumPolicy>} in a transitive POM) is not honored and a warning is logged 236 * instead. Enabling this restores the legacy weakest-wins merge, which let any POM in the dependency graph 237 * degrade or switch off checksum verification for downloads routed through the mirror. 238 * 239 * @since 2.0.23 240 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 241 * @configurationType {@link java.lang.Boolean} 242 * @configurationDefaultValue {@link #DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE} 243 */ 244 public static final String CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE = 245 "aether.remoteRepositoryManager.rawRepositoryChecksumPolicyDowngrade"; 246 247 public static final boolean DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE = false; 248 249 private static RepositoryPolicy clampChecksumPolicy( 250 RepositorySystemSession session, RepositoryPolicy merged, RepositoryPolicy floor, RemoteRepository rec) { 251 if (merged == null || floor == null) { 252 return merged; 253 } 254 if (checksumPolicyRank(merged.getChecksumPolicy()) < checksumPolicyRank(floor.getChecksumPolicy())) { 255 logWarnOnce( 256 session, 257 "Ignoring checksum policy '{}' contributed by repository {} ({}) declared by a remote artifact" 258 + " descriptor: it would downgrade the checksum policy '{}' of the mirror serving it;" 259 + " set {}=true to restore the legacy weakest-wins merge", 260 merged.getChecksumPolicy(), 261 rec.getId(), 262 rec.getUrl(), 263 floor.getChecksumPolicy(), 264 CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE); 265 return new RepositoryPolicy( 266 merged.isEnabled(), 267 merged.getArtifactUpdatePolicy(), 268 merged.getMetadataUpdatePolicy(), 269 floor.getChecksumPolicy()); 270 } 271 return merged; 272 } 273 274 private static int checksumPolicyRank(String policy) { 275 if (policy == null) { 276 return -1; 277 } 278 switch (policy) { 279 case RepositoryPolicy.CHECKSUM_POLICY_FAIL: 280 return 2; 281 case RepositoryPolicy.CHECKSUM_POLICY_WARN: 282 return 1; 283 case RepositoryPolicy.CHECKSUM_POLICY_IGNORE: 284 return 0; 285 default: 286 // unknown (potentially attacker-supplied) values rank below any known policy, so they can never 287 // displace an operator-configured one 288 return -1; 289 } 290 } 291 292 private RemoteRepository mergeMirrors( 293 RepositorySystemSession session, 294 RepositoryKeyFunction repositoryKeyFunction, 295 RemoteRepository dominant, 296 RemoteRepository recessive, 297 boolean recessiveIsRaw) { 298 boolean rawChecksumPolicyDowngrade = org.eclipse.aether.util.ConfigUtils.getBoolean( 299 session, DEFAULT_RAW_CHECKSUM_POLICY_DOWNGRADE, CONFIG_PROP_RAW_CHECKSUM_POLICY_DOWNGRADE); 300 RemoteRepository.Builder merged = null; 301 RepositoryPolicy releases = null, snapshots = null; 302 303 next: 304 for (RemoteRepository rec : recessive.getMirroredRepositories()) { 305 String recKey = repositoryKeyFunction.apply(rec, null); 306 307 for (RemoteRepository dom : dominant.getMirroredRepositories()) { 308 if (recKey.equals(repositoryKeyFunction.apply(dom, null))) { 309 continue next; 310 } 311 } 312 313 if (merged == null) { 314 merged = new RemoteRepository.Builder(dominant); 315 releases = dominant.getPolicy(false); 316 snapshots = dominant.getPolicy(true); 317 } 318 319 releases = merge(session, releases, rec.getPolicy(false), false); 320 snapshots = merge(session, snapshots, rec.getPolicy(true), false); 321 322 if (recessiveIsRaw && !rawChecksumPolicyDowngrade) { 323 // "recessive" originates from a remote artifact descriptor (POM): remotely supplied input must 324 // never weaken the checksum policy the operator configured on the mirror itself 325 releases = clampChecksumPolicy(session, releases, dominant.getPolicy(false), rec); 326 snapshots = clampChecksumPolicy(session, snapshots, dominant.getPolicy(true), rec); 327 } 328 329 merged.addMirroredRepository(rec); 330 } 331 332 if (merged == null) { 333 return dominant; 334 } 335 return merged.setReleasePolicy(releases).setSnapshotPolicy(snapshots).build(); 336 } 337 338 /** 339 * Flag indicating whether the merge of a repository's release and snapshot policies (used when a single 340 * effective policy has to serve both natures, most notably for metadata of nature RELEASE_OR_SNAPSHOT such as 341 * {@code maven-metadata.xml} version lists) may pick the <em>weaker</em> of the two checksum policies, which was 342 * the legacy behavior. When disabled (the default), the stronger of the two checksum policies wins, so enabling 343 * snapshots with a lenient checksum policy no longer silently downgrades checksum enforcement below what the 344 * operator configured for releases (or vice versa). An explicit checksum policy set on the session (e.g. via 345 * {@code --strict-checksums}) takes precedence over either behavior, as before. 346 * 347 * @since 2.0.23 348 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 349 * @configurationType {@link java.lang.Boolean} 350 * @configurationDefaultValue {@link #DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY} 351 */ 352 public static final String CONFIG_PROP_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY = 353 "aether.remoteRepositoryManager.natureMergeWeakestChecksumPolicy"; 354 355 public static final boolean DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY = false; 356 357 @Override 358 public RepositoryPolicy getPolicy( 359 RepositorySystemSession session, RemoteRepository repository, boolean releases, boolean snapshots) { 360 requireNonNull(session, "session cannot be null"); 361 requireNonNull(repository, "repository cannot be null"); 362 RepositoryPolicy policy1 = releases ? repository.getPolicy(false) : null; 363 RepositoryPolicy policy2 = snapshots ? repository.getPolicy(true) : null; 364 return merge(session, policy1, policy2, true); 365 } 366 367 private RepositoryPolicy merge( 368 RepositorySystemSession session, RepositoryPolicy policy1, RepositoryPolicy policy2, boolean globalPolicy) { 369 RepositoryPolicy policy; 370 371 if (policy2 == null) { 372 if (globalPolicy) { 373 policy = merge( 374 policy1, 375 session.getArtifactUpdatePolicy(), 376 session.getMetadataUpdatePolicy(), 377 session.getChecksumPolicy()); 378 } else { 379 policy = policy1; 380 } 381 } else if (policy1 == null) { 382 if (globalPolicy) { 383 policy = merge( 384 policy2, 385 session.getArtifactUpdatePolicy(), 386 session.getMetadataUpdatePolicy(), 387 session.getChecksumPolicy()); 388 } else { 389 policy = policy2; 390 } 391 } else if (!policy2.isEnabled()) { 392 if (globalPolicy) { 393 policy = merge( 394 policy1, 395 session.getArtifactUpdatePolicy(), 396 session.getMetadataUpdatePolicy(), 397 session.getChecksumPolicy()); 398 } else { 399 policy = policy1; 400 } 401 } else if (!policy1.isEnabled()) { 402 if (globalPolicy) { 403 policy = merge( 404 policy2, 405 session.getArtifactUpdatePolicy(), 406 session.getMetadataUpdatePolicy(), 407 session.getChecksumPolicy()); 408 } else { 409 policy = policy2; 410 } 411 } else { 412 String checksums = session.getChecksumPolicy(); 413 //noinspection StatementWithEmptyBody 414 if (globalPolicy && checksums != null && !checksums.isEmpty()) { 415 // use global override 416 } else if (globalPolicy 417 && !org.eclipse.aether.util.ConfigUtils.getBoolean( 418 session, 419 DEFAULT_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY, 420 CONFIG_PROP_NATURE_MERGE_WEAKEST_CHECKSUM_POLICY)) { 421 // merging the release and snapshot policies of a single repository into one effective policy 422 // (e.g. for metadata of nature RELEASE_OR_SNAPSHOT): the result must not be weaker than what the 423 // operator configured for either nature, so the stronger checksum policy wins 424 checksums = strongerChecksumPolicy(policy1.getChecksumPolicy(), policy2.getChecksumPolicy()); 425 } else { 426 checksums = checksumPolicyProvider.getEffectiveChecksumPolicy( 427 session, policy1.getChecksumPolicy(), policy2.getChecksumPolicy()); 428 } 429 430 String artifactUpdates = session.getArtifactUpdatePolicy(); 431 //noinspection StatementWithEmptyBody 432 if (globalPolicy && artifactUpdates != null && !artifactUpdates.isEmpty()) { 433 // use global override 434 } else { 435 artifactUpdates = updatePolicyAnalyzer.getEffectiveUpdatePolicy( 436 session, policy1.getArtifactUpdatePolicy(), policy2.getArtifactUpdatePolicy()); 437 } 438 String metadataUpdates = session.getMetadataUpdatePolicy(); 439 if (globalPolicy && metadataUpdates != null && !metadataUpdates.isEmpty()) { 440 // use global override 441 } else { 442 metadataUpdates = updatePolicyAnalyzer.getEffectiveUpdatePolicy( 443 session, policy1.getMetadataUpdatePolicy(), policy2.getMetadataUpdatePolicy()); 444 } 445 446 policy = new RepositoryPolicy(true, artifactUpdates, metadataUpdates, checksums); 447 } 448 449 return policy; 450 } 451 452 private RepositoryPolicy merge( 453 RepositoryPolicy policy, String artifactUpdates, String metadataUpdates, String checksums) { 454 if (policy != null) { 455 if (artifactUpdates == null || artifactUpdates.isEmpty()) { 456 artifactUpdates = policy.getArtifactUpdatePolicy(); 457 } 458 if (metadataUpdates == null || metadataUpdates.isEmpty()) { 459 metadataUpdates = policy.getMetadataUpdatePolicy(); 460 } 461 if (checksums == null || checksums.isEmpty()) { 462 checksums = policy.getChecksumPolicy(); 463 } 464 if (!policy.getArtifactUpdatePolicy().equals(artifactUpdates) 465 || !policy.getMetadataUpdatePolicy().equals(metadataUpdates) 466 || !policy.getChecksumPolicy().equals(checksums)) { 467 policy = new RepositoryPolicy(policy.isEnabled(), artifactUpdates, metadataUpdates, checksums); 468 } 469 } 470 return policy; 471 } 472 473 private static String strongerChecksumPolicy(String policy1, String policy2) { 474 if (checksumPolicyStrength(policy2) > checksumPolicyStrength(policy1)) { 475 return policy2; 476 } 477 return policy1; 478 } 479 480 private static int checksumPolicyStrength(String policy) { 481 if (policy == null) { 482 return -1; 483 } 484 switch (policy) { 485 case RepositoryPolicy.CHECKSUM_POLICY_FAIL: 486 return 2; 487 case RepositoryPolicy.CHECKSUM_POLICY_WARN: 488 return 1; 489 case RepositoryPolicy.CHECKSUM_POLICY_IGNORE: 490 return 0; 491 default: 492 // unknown values never win a strength comparison; they are rejected downstream when a 493 // ChecksumPolicy instance is created for them 494 return -1; 495 } 496 } 497 498 @SuppressWarnings("unchecked") 499 private static void logWarnOnce(RepositorySystemSession session, String message, Object... args) { 500 if (!LOGGER.isWarnEnabled()) { 501 return; 502 } 503 Object[] keys = new Object[args.length + 1]; 504 keys[0] = message; 505 System.arraycopy(args, 0, keys, 1, args.length); 506 Object key = Keys.of(keys); 507 ((Map<Object, Boolean>) session.getData() 508 .computeIfAbsent( 509 Keys.of(DefaultRemoteRepositoryManager.class, "logWarnOnce"), ConcurrentHashMap::new)) 510 .computeIfAbsent(key, k -> { 511 LOGGER.warn(message, args); 512 return true; 513 }); 514 } 515}