Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;

import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;
import static org.openrewrite.internal.StreamUtils.readAllBytes;
Expand Down Expand Up @@ -110,15 +112,35 @@ public MavenArtifactDownloader(MavenArtifactCache mavenArtifactCache,
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 {
byte[] responseBytes = null;
int responseCode;
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);
}
}
// Fall back to anonymous if authenticated request fails with a client error
if (responseBytes == null && isClientSideError(responseCode) && hasAuthentication(dependency.getRepository())) {
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);
}
}
}
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,
Expand All @@ -130,14 +152,44 @@ public MavenArtifactDownloader(MavenArtifactCache mavenArtifactCache,
}

private HttpSender.Request.Builder applyAuthentication(MavenRepository repository, HttpSender.Request.Builder request) {
for (MavenSettings.HttpHeader header : resolveHttpHeaders(repository)) {
request.withHeader(header.getName(), header.getValue());
}
String[] credentials = resolveCredentials(repository);
if (credentials != null) {
return request.withBasicAuthentication(credentials[0], credentials[1]);
}
return request;
}

private boolean hasAuthentication(MavenRepository repository) {
return !resolveHttpHeaders(repository).isEmpty() || resolveCredentials(repository) != null;
}

/**
* All 400s are client-side errors, but 408 (timeout), 425 (too early) and 429 (too many requests) are transient
* rather than credential rejections, so an anonymous retry is pointless for them. Mirrors
* {@code MavenPomDownloader.HttpSenderResponseException#isClientSideException()}.
*/
private static boolean isClientSideError(int responseCode) {
if (responseCode < 400 || responseCode > 499) {
return false;
}
return responseCode != 408 && responseCode != 425 && responseCode != 429;
}

private List<MavenSettings.HttpHeader> resolveHttpHeaders(MavenRepository repository) {
MavenSettings.Server authInfo = serverIdToServer.get(repository.getId());
if (authInfo != null && authInfo.getConfiguration() != null && authInfo.getConfiguration().getHttpHeaders() != null) {
return authInfo.getConfiguration().getHttpHeaders();
}
return emptyList();
}

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 {
Expand All @@ -146,8 +198,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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,177 @@ void downloadDependenciesWithClassifier(@TempDir Path tempDir) {
}
}

@Test
void fallsBackToAnonymousWhenServerReturns401(@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
"""
<settings>
<servers>
<server>
<id>mock-repo</id>
<username>bad-user</username>
<password>bad-password</password>
</server>
</servers>
</settings>
""".getBytes())), new InMemoryExecutionContext());

MavenArtifactCache artifactCache = new LocalMavenArtifactCache(tempDir);
AtomicReference<Throwable> 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")).isNotNull();
assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNull();
}
}

@Test
void fallsBackToAnonymousWhenHttpHeaderAuthRejected(@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("X-Auth-Token") != 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
"""
<settings>
<servers>
<server>
<id>mock-repo</id>
<configuration>
<httpHeaders>
<property>
<name>X-Auth-Token</name>
<value>bad-token</value>
</property>
</httpHeaders>
</configuration>
</server>
</servers>
</settings>
""".getBytes())), new InMemoryExecutionContext());

MavenArtifactCache artifactCache = new LocalMavenArtifactCache(tempDir);
AtomicReference<Throwable> 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("X-Auth-Token")).isNotNull();
assertThat(mockRepo.takeRequest().getHeader("X-Auth-Token")).isNull();
}
}

@Test
void doesNotFallBackToAnonymousOnTransientClientError(@TempDir Path tempDir) throws Exception {
try (MockWebServer mockRepo = new MockWebServer()) {
mockRepo.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
// 429 is a transient rate-limit, not a credential rejection, so no anonymous retry should follow
return new MockResponse().setResponseCode(429);
}
});
mockRepo.start();

String repoUrl = "http://" + mockRepo.getHostName() + ":" + mockRepo.getPort();
MavenSettings settings = MavenSettings.parse(new Parser.Input(
Path.of("settings.xml"), () -> new ByteArrayInputStream(
//language=xml
"""
<settings>
<servers>
<server>
<id>mock-repo</id>
<username>bad-user</username>
<password>bad-password</password>
</server>
</servers>
</settings>
""".getBytes())), new InMemoryExecutionContext());

MavenArtifactCache artifactCache = new LocalMavenArtifactCache(tempDir);
AtomicReference<Throwable> 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).isNull();
assertThat(error.get()).isNotNull();
assertThat(mockRepo.getRequestCount()).isEqualTo(1);
assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNotNull();
}
}

@Test
void fallsBackToAnonymousWhenCredentialsRejected(@TempDir Path tempDir) throws Exception {
byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; // minimal ZIP magic bytes
Expand Down
Loading