diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java b/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java index 5777978f2d3..b8b34a50690 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java @@ -85,7 +85,7 @@ private Path dependencyPath(ResolvedDependency dependency) { return resolvedPath.resolve(dependency.getArtifactId() + "-" + (dependency.getDatedSnapshotVersion() == null ? dependency.getVersion() : dependency.getDatedSnapshotVersion()) + - (dependency.getRequested().getClassifier() == null ? "" : dependency.getRequested().getClassifier()) + + (dependency.getRequested().getClassifier() == null ? "" : "-" + dependency.getRequested().getClassifier()) + ".jar"); } } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java b/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java index b675c991d7c..57a971d708e 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java @@ -109,16 +109,37 @@ public MavenArtifactDownloader(MavenArtifactCache mavenArtifactCache, } else if ("file".equals(URI.create(uri).getScheme())) { bodyStream = Files.newInputStream(Paths.get(URI.create(uri))); } else { - HttpSender.Request.Builder request = applyAuthentication(dependency.getRepository(), httpSender.get(uri)); - try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(request.build())); - InputStream body = response.getBody()) { - if (!response.isSuccessful() || body == null) { - onError.accept(new MavenDownloadingException(String.format("Unable to download dependency %s:%s:%s from %s. Response was %d", - dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), uri, response.getCode()), null, + try { + // Try anonymously first, mirroring Apache Maven's DeferredCredentialsProvider behavior + byte[] responseBytes = null; + int responseCode; + try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(httpSender.get(uri).build())); + InputStream body = response.getBody()) { + responseCode = response.getCode(); + if (response.isSuccessful() && body != null) { + responseBytes = readAllBytes(body); + } + } + // Retry with credentials if the anonymous request failed with a 4xx and we have credentials + if (responseBytes == null && responseCode >= 400 && responseCode < 500 && hasCredentials(dependency.getRepository())) { + HttpSender.Request.Builder request = applyAuthentication(dependency.getRepository(), httpSender.get(uri)); + try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(request.build())); + InputStream body = response.getBody()) { + responseCode = response.getCode(); + if (response.isSuccessful() && body != null) { + responseBytes = readAllBytes(body); + } + } + } + if (responseBytes == null) { + onError.accept(new MavenDownloadingException(String.format("Unable to download dependency %s:%s:%s%s from %s. Response was %d", + dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), + dependency.getClassifier() == null ? "" : ":" + dependency.getClassifier(), + uri, responseCode), null, dependency.getRequested().getGav())); return null; } - bodyStream = new ByteArrayInputStream(readAllBytes(body)); + bodyStream = new ByteArrayInputStream(responseBytes); } catch (Throwable t) { Throwable cause = t instanceof FailsafeException && t.getCause() != null ? t.getCause() : t; throw new MavenDownloadingException("Unable to download dependency", cause, @@ -130,14 +151,27 @@ public MavenArtifactDownloader(MavenArtifactCache mavenArtifactCache, } private HttpSender.Request.Builder applyAuthentication(MavenRepository repository, HttpSender.Request.Builder request) { + MavenSettings.Server authInfo = serverIdToServer.get(repository.getId()); + if (authInfo != null && authInfo.getConfiguration() != null && authInfo.getConfiguration().getHttpHeaders() != null) { + for (MavenSettings.HttpHeader header : authInfo.getConfiguration().getHttpHeaders()) { + request.withHeader(header.getName(), header.getValue()); + } + } + String[] credentials = resolveCredentials(repository); + if (credentials != null) { + return request.withBasicAuthentication(credentials[0], credentials[1]); + } + return request; + } + + private boolean hasCredentials(MavenRepository repository) { + return resolveCredentials(repository) != null; + } + + private String @Nullable [] resolveCredentials(MavenRepository repository) { String username, password; MavenSettings.Server authInfo = serverIdToServer.get(repository.getId()); if (authInfo != null) { - if (authInfo.getConfiguration() != null && authInfo.getConfiguration().getHttpHeaders() != null) { - for (MavenSettings.HttpHeader header : authInfo.getConfiguration().getHttpHeaders()) { - request.withHeader(header.getName(), header.getValue()); - } - } username = authInfo.getUsername(); password = authInfo.getPassword(); } else { @@ -146,8 +180,8 @@ private HttpSender.Request.Builder applyAuthentication(MavenRepository repositor } if (username != null && !username.contains("${") && password != null && !password.contains("${")) { - return request.withBasicAuthentication(username, password); + return new String[]{username, password}; } - return request; + return null; } } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java index a501181570f..69dbd0ff0ce 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java @@ -133,7 +133,7 @@ void downloadDependenciesWithClassifier(@TempDir Path tempDir) { } @Test - void fallsBackToAnonymousWhenCredentialsRejected(@TempDir Path tempDir) throws Exception { + void publicArtifactsResolveAnonymouslyEvenWhenCredentialsAreInvalid(@TempDir Path tempDir) throws Exception { byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; // minimal ZIP magic bytes try (MockWebServer mockRepo = new MockWebServer()) { @@ -141,7 +141,7 @@ void fallsBackToAnonymousWhenCredentialsRejected(@TempDir Path tempDir) throws E @Override public MockResponse dispatch(RecordedRequest request) { if (request.getHeader("Authorization") != null) { - return new MockResponse().setResponseCode(403); // Throw if used; it should not be called at all + return new MockResponse().setResponseCode(401); } return new MockResponse().setResponseCode(200) .setBody(new okio.Buffer().write(jarBytes)); @@ -158,8 +158,8 @@ public MockResponse dispatch(RecordedRequest request) { mock-repo - ${placeholder} - ${placeholder} + bad-user + bad-password @@ -184,6 +184,64 @@ public MockResponse dispatch(RecordedRequest request) { assertThat(artifact).isNotNull(); assertThat(error.get()).isNull(); assertThat(mockRepo.getRequestCount()).isEqualTo(1); + assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNull(); + } + } + + @Test + void retriesWithCredentialsWhenAnonymousReturns401(@TempDir Path tempDir) throws Exception { + byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; + + try (MockWebServer mockRepo = new MockWebServer()) { + mockRepo.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + if (request.getHeader("Authorization") == null) { + return new MockResponse().setResponseCode(401); + } + return new MockResponse().setResponseCode(200) + .setBody(new okio.Buffer().write(jarBytes)); + } + }); + mockRepo.start(); + + String repoUrl = "http://" + mockRepo.getHostName() + ":" + mockRepo.getPort(); + MavenSettings settings = MavenSettings.parse(new Parser.Input( + Path.of("settings.xml"), () -> new ByteArrayInputStream( + //language=xml + """ + + + + mock-repo + good-user + good-password + + + + """.getBytes())), new InMemoryExecutionContext()); + + MavenArtifactCache artifactCache = new LocalMavenArtifactCache(tempDir); + AtomicReference error = new AtomicReference<>(); + MavenArtifactDownloader downloader = new MavenArtifactDownloader( + artifactCache, settings, error::set); + + MavenRepository repo = new MavenRepository( + "mock-repo", repoUrl, "true", "false", true, null, null, null, false); + GroupArtifactVersion gav = new GroupArtifactVersion("com.example", "test-lib", "1.0.0"); + ResolvedDependency dep = ResolvedDependency.builder() + .repository(repo) + .gav(new ResolvedGroupArtifactVersion(repoUrl, gav.getGroupId(), gav.getArtifactId(), gav.getVersion(), null)) + .requested(Dependency.builder().gav(gav).build()) + .build(); + + Path artifact = downloader.downloadArtifact(dep); + + assertThat(artifact).isNotNull(); + assertThat(error.get()).isNull(); + assertThat(mockRepo.getRequestCount()).isEqualTo(2); + assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNull(); + assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNotNull(); } } }