Skip to content

Commit

Permalink
Merge pull request #15868 from iterate-ch/bugfix/GH-15127-authcache
Browse files Browse the repository at this point in the history
Only share context in subsequent logical requests
  • Loading branch information
dkocher authored Apr 29, 2024
2 parents 48d46ae + 9796900 commit cdd8b08
Show file tree
Hide file tree
Showing 15 changed files with 447 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
* feedback@cyberduck.io
*/

import ch.cyberduck.core.Host;
import ch.cyberduck.core.PreferencesUseragentProvider;
import ch.cyberduck.core.TranscriptListener;
import ch.cyberduck.core.UseragentProvider;
import ch.cyberduck.core.exception.RetriableAccessDeniedException;
import ch.cyberduck.core.preferences.HostPreferences;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
Expand All @@ -29,20 +32,28 @@
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;

import java.io.IOException;
import java.util.Arrays;

public class LoggingHttpRequestExecutor extends HttpRequestExecutor {
public class CustomHttpRequestExecutor extends HttpRequestExecutor {

private final UseragentProvider useragentProvider
= new PreferencesUseragentProvider();

private final Host host;
private final TranscriptListener listener;

public LoggingHttpRequestExecutor(final TranscriptListener listener) {
public CustomHttpRequestExecutor(final Host host, final TranscriptListener listener) {
this.host = host;
this.listener = listener;
}

Expand Down Expand Up @@ -77,8 +88,29 @@ protected HttpResponse doSendRequest(final HttpRequest request, final HttpClient
}
final HttpResponse response = super.doSendRequest(request, conn, context);
if(null != response) {
// response received as part of an expect-continue handshake
// Response received as part of an expect-continue handshake
this.log(response);
if(new HostPreferences(host).getBoolean("request.unauthorized.ntlm.preflight")) {
// Unable to authenticate with NTLM for requests with non-zero entity
if(HttpStatus.SC_UNAUTHORIZED == response.getStatusLine().getStatusCode()) {
if(response.containsHeader(HttpHeaders.WWW_AUTHENTICATE)) {
switch(request.getRequestLine().getMethod()) {
case HttpPut.METHOD_NAME:
case HttpPost.METHOD_NAME:
case HttpPatch.METHOD_NAME:
if(Arrays.asList(response.getAllHeaders()).stream()
.filter(header -> HttpHeaders.WWW_AUTHENTICATE.equals(header.getName()))
.filter(header -> "NTLM".equals(header.getValue())).findAny().isPresent()) {
// Unauthenticated connection cannot proceed with PUT
final HttpResponseException preflight = new HttpResponseException(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
preflight.initCause(new RetriableAccessDeniedException(String.format("Authentication cannot proceed for %s",
request.getRequestLine().getMethod())));
throw preflight;
}
}
}
}
}
}
return response;
}
Expand Down
11 changes: 2 additions & 9 deletions core/src/main/java/ch/cyberduck/core/http/DelayedHttpEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,13 @@ public DelayedHttpEntity(final Thread parentThread, final CountDownLatch streamO
*/
private OutputStream stream;

/**
* Entity written to server
*/
private boolean entityWritten = false;

/**
* Parent thread to check if still alive
*/
private final Thread parentThread;

public boolean isRepeatable() {
return !entityWritten;
return false;
}

public abstract long getContentLength();
Expand Down Expand Up @@ -120,12 +115,10 @@ protected void handleIOException(final IOException e) throws IOException {
}
// Wait for signal when content has been written to the pipe
Interruptibles.await(streamClosed, IOException.class, new Interruptibles.ThreadAliveCancelCallback(parentThread));
// Entity written to server
entityWritten = true;
}

public boolean isStreaming() {
return !entityWritten;
return true;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ public HttpClientBuilder build(final Proxy proxy, final TranscriptListener liste
else {
configuration.setConnectionReuseStrategy(new NoConnectionReuseStrategy());
}
if(!new HostPreferences(host).getBoolean("http.connections.state.enable")) {
configuration.disableConnectionState();
}
// Retry handler for I/O failures
configuration.setRetryHandler(new ExtendedHttpRequestRetryHandler(
new HostPreferences(host).getInteger("connection.retry")));
Expand All @@ -181,7 +184,7 @@ public HttpClientBuilder build(final Proxy proxy, final TranscriptListener liste
if(!new HostPreferences(host).getBoolean("http.compression.enable")) {
configuration.disableContentCompression();
}
configuration.setRequestExecutor(new LoggingHttpRequestExecutor(listener));
configuration.setRequestExecutor(new CustomHttpRequestExecutor(host, listener));
// Always register HTTP for possible use with proxy. Contains a number of protocol properties such as the
// default port and the socket factory to be used to create the java.net.Socket instances for the given protocol
final PoolingHttpClientConnectionManager connectionManager = this.createConnectionManager(this.createRegistry());
Expand Down
2 changes: 2 additions & 0 deletions defaults/src/main/resources/default.properties
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ http.compression.enable=true
# Integer.MAX_VALUE
http.connections.route=2147483647
http.connections.reuse=true
http.connections.state.enable=false
http.connections.stale.check.ms=5000

# Total number of connections in the pool
Expand All @@ -248,6 +249,7 @@ http.socket.buffer=8192
http.credentials.charset=UTF-8
http.request.uri.normalize=false
http.request.entity.buffer.limit=5242880
request.unauthorized.ntlm.preflight=false

# Enable or disable verification that the remote host taking part
# of a data connection is the same as the host to which the control
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public NextcloudWriteFeature(final DAVSession session) {
}

@Override
protected List<Header> getHeaders(final Path file, final TransferStatus status) throws UnsupportedException {
final List<Header> headers = super.getHeaders(file, status);
protected List<Header> toHeaders(final Path file, final TransferStatus status, final boolean expectdirective) throws UnsupportedException {
final List<Header> headers = super.toHeaders(file, status, expectdirective);
if(null != status.getModified()) {
headers.add(new BasicHeader("X-OC-Mtime", String.valueOf(status.getModified() / 1000)));
}
Expand Down
37 changes: 35 additions & 2 deletions webdav/src/main/java/ch/cyberduck/core/dav/DAVClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,27 @@
import ch.cyberduck.core.preferences.PreferencesFactory;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -52,11 +60,30 @@ public class DAVClient extends SardineImpl {

private final String uri;

private final BasicAuthCache authCache = new BasicAuthCache();
private final CredentialsProvider authProvider = new BasicCredentialsProvider();

public DAVClient(final String uri, final HttpClientBuilder http) {
super(http);
this.uri = uri;
}

public void setCredentials(final AuthScope authScope, final Credentials credentials) {
authProvider.setCredentials(authScope, credentials);
}

@Override
public void enablePreemptiveAuthentication(final String hostname, final int httpPort, final int httpsPort, final Charset credentialsCharset) {
final BasicScheme basicScheme = new BasicScheme(credentialsCharset);
authCache.put(new HttpHost(hostname, httpPort, "http"), basicScheme);
authCache.put(new HttpHost(hostname, httpsPort, "https"), basicScheme);
}

@Override
public void disablePreemptiveAuthentication() {
authCache.clear();
}

@Override
public <T> T execute(final HttpRequestBase request, final ResponseHandler<T> responseHandler) throws IOException {
if(StringUtils.isNotBlank(request.getURI().getRawQuery())) {
Expand All @@ -65,7 +92,10 @@ public <T> T execute(final HttpRequestBase request, final ResponseHandler<T> res
else {
request.setURI(URI.create(String.format("%s%s", uri, request.getURI().getRawPath())));
}
return super.execute(request, responseHandler);
final HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(authProvider);
context.setAuthCache(authCache);
return this.execute(context, request, responseHandler);
}

@Override
Expand All @@ -76,7 +106,10 @@ protected HttpResponse execute(final HttpRequestBase request) throws IOException
else {
request.setURI(URI.create(String.format("%s%s", uri, request.getURI().getRawPath())));
}
return super.execute(request);
final HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(authProvider);
context.setAuthCache(authCache);
return this.execute(context, request, null);
}

@Override
Expand Down
Loading

0 comments on commit cdd8b08

Please sign in to comment.