diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java index 959d28766..1e2994700 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java @@ -312,6 +312,22 @@ public final class ConfigurationProperties { */ public static final boolean DEFAULT_HTTP_PREEMPTIVE_PUT_AUTH = true; + /** + * Boolean flag should the HTTP transport use expect-continue handshake for PUT requests. Not all transport support + * this option. This option may be needed for some broken HTTP servers. + * + * @see #DEFAULT_HTTP_EXPECT_CONTINUE + * @since 2.0.0 + */ + public static final String HTTP_EXPECT_CONTINUE = PREFIX_CONNECTOR + "http.expectContinue"; + + /** + * Default value if {@link #HTTP_EXPECT_CONTINUE} is not set: {@code true}. + * + * @since 2.0.0 + */ + public static final boolean DEFAULT_HTTP_EXPECT_CONTINUE = true; + /** * The mode that sets HTTPS transport "security mode": to ignore any SSL errors (certificate validity checks, * hostname verification). The default value is {@link #HTTPS_SECURITY_MODE_DEFAULT}. diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java index 9bd588986..fc16daab8 100644 --- a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java +++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java @@ -322,6 +322,15 @@ final class HttpTransporter extends AbstractTransporter { builder.useSystemProperties(); } + final boolean expectContinue = ConfigUtils.getBoolean( + session, + ConfigurationProperties.DEFAULT_HTTP_EXPECT_CONTINUE, + ConfigurationProperties.HTTP_EXPECT_CONTINUE + "." + repository.getId(), + ConfigurationProperties.HTTP_EXPECT_CONTINUE); + if (expectContinue != ConfigurationProperties.DEFAULT_HTTP_EXPECT_CONTINUE) { + state.setExpectContinue(expectContinue); + } + final boolean reuseConnections = ConfigUtils.getBoolean( session, ConfigurationProperties.DEFAULT_HTTP_REUSE_CONNECTIONS, diff --git a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java index 1764a6a5d..94f5d76fd 100644 --- a/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java +++ b/maven-resolver-transport-http/src/test/java/org/eclipse/aether/transport/http/HttpTransporterTest.java @@ -726,6 +726,27 @@ void testPut_Authenticated_ExpectContinueRejected() throws Exception { assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); } + @Test + void testPut_Authenticated_ExpectContinueDisabled() throws Exception { + session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false); + httpServer.setAuthentication("testuser", "testpass"); + httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode + auth = new AuthenticationBuilder() + .addUsername("testuser") + .addPassword("testpass") + .build(); + newTransporter(httpServer.getHttpUrl()); + RecordingTransportListener listener = new RecordingTransportListener(); + PutTask task = + new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload"); + transporter.put(task); + assertEquals(0L, listener.dataOffset); + assertEquals(6L, listener.dataLength); + assertEquals(1, listener.startedCount); // w/ expectContinue enabled would have here 2 + assertTrue(listener.progressedCount > 0, "Count: " + listener.progressedCount); + assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt"))); + } + @Test void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception { Map headers = new HashMap<>(); diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java index ece185172..bb2f97ea6 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java @@ -103,6 +103,8 @@ final class JdkHttpTransporter extends AbstractTransporter { private final int requestTimeout; + private final boolean expectContinue; + JdkHttpTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { try { URI uri = new URI(repository.getUrl()).parseServerAuthority(); @@ -149,6 +151,11 @@ final class JdkHttpTransporter extends AbstractTransporter { ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), ConfigurationProperties.REQUEST_TIMEOUT); + this.expectContinue = ConfigUtils.getBoolean( + session, + ConfigurationProperties.DEFAULT_HTTP_EXPECT_CONTINUE, + ConfigurationProperties.HTTP_EXPECT_CONTINUE + "." + repository.getId(), + ConfigurationProperties.HTTP_EXPECT_CONTINUE); this.headers = headers; this.client = getOrCreateClient(session, repository); @@ -280,8 +287,10 @@ protected void implGet(GetTask task) throws Exception { @Override protected void implPut(PutTask task) throws Exception { - HttpRequest.Builder request = - HttpRequest.newBuilder().uri(resolve(task)).timeout(Duration.ofMillis(requestTimeout)); + HttpRequest.Builder request = HttpRequest.newBuilder() + .uri(resolve(task)) + .timeout(Duration.ofMillis(requestTimeout)) + .expectContinue(expectContinue); headers.forEach(request::setHeader); try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) { utilPut(task, Files.newOutputStream(tempFile.getPath()), true); diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index e34f4e343..cc4c96feb 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -38,6 +38,7 @@ Option | Type | Description | Default Value | Supports Repo ID Suffix `aether.connector.http.cacheState` | boolean | Flag indicating whether a memory-based cache is used for user tokens, connection managers, expect continue requests and authentication schemes. | `true` | no `aether.connector.http.connectionMaxTtl` | int | Total time to live in seconds for an HTTP connection, after that time, the connection will be dropped (no matter for how long it was idle). | `300` | yes `aether.connector.http.credentialEncoding` | String | The encoding/charset to use when exchanging credentials with HTTP servers. | `"ISO-8859-1"` | yes +`aether.connector.http.expectContinue` | boolean | Whether to use expect/continue handshake during PUTs. Some broken HTTP servers needs this disabled. | `true` | yes `aether.connector.http.headers` | `Map` | The request headers to use for HTTP-based repository connectors. The headers are specified using a map of strings mapping a header name to its value. The repository-specific headers map is supposed to be complete, i.e. is not merged with the general headers map. | - | yes `aether.connector.http.localAddress` | String | Set the outgoing interface (globally or per remote repository). Valid values are local accessible IP addresses or host names. The default will use the system's default route. Invalid addresses will result in HttpTransport creation failure. | - | yes `aether.connector.http.maxConnectionsPerRoute` | int | The maximum concurrent connections per route HTTP client is allowed to use. | `50` | yes