| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven.wagon.shared.http; |
| |
| import javax.net.ssl.HttpsURLConnection; |
| import javax.net.ssl.SSLContext; |
| |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.RandomAccessFile; |
| import java.nio.Buffer; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.Channels; |
| import java.nio.channels.ReadableByteChannel; |
| import java.nio.charset.StandardCharsets; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.TimeZone; |
| import java.util.concurrent.TimeUnit; |
| |
| import org.apache.http.Header; |
| import org.apache.http.HttpEntity; |
| import org.apache.http.HttpException; |
| import org.apache.http.HttpHost; |
| import org.apache.http.HttpStatus; |
| import org.apache.http.auth.AuthSchemeProvider; |
| import org.apache.http.auth.AuthScope; |
| import org.apache.http.auth.ChallengeState; |
| import org.apache.http.auth.Credentials; |
| import org.apache.http.auth.NTCredentials; |
| import org.apache.http.auth.UsernamePasswordCredentials; |
| import org.apache.http.client.AuthCache; |
| import org.apache.http.client.CredentialsProvider; |
| import org.apache.http.client.HttpRequestRetryHandler; |
| import org.apache.http.client.ServiceUnavailableRetryStrategy; |
| import org.apache.http.client.config.AuthSchemes; |
| import org.apache.http.client.config.CookieSpecs; |
| import org.apache.http.client.config.RequestConfig; |
| import org.apache.http.client.methods.CloseableHttpResponse; |
| import org.apache.http.client.methods.HttpGet; |
| import org.apache.http.client.methods.HttpHead; |
| import org.apache.http.client.methods.HttpPut; |
| import org.apache.http.client.methods.HttpUriRequest; |
| import org.apache.http.client.protocol.HttpClientContext; |
| import org.apache.http.client.utils.DateUtils; |
| import org.apache.http.config.Registry; |
| import org.apache.http.config.RegistryBuilder; |
| import org.apache.http.conn.HttpClientConnectionManager; |
| import org.apache.http.conn.socket.ConnectionSocketFactory; |
| import org.apache.http.conn.socket.PlainConnectionSocketFactory; |
| import org.apache.http.conn.ssl.SSLConnectionSocketFactory; |
| import org.apache.http.conn.ssl.SSLContextBuilder; |
| import org.apache.http.conn.ssl.SSLInitializationException; |
| import org.apache.http.entity.AbstractHttpEntity; |
| import org.apache.http.impl.auth.BasicScheme; |
| import org.apache.http.impl.auth.BasicSchemeFactory; |
| import org.apache.http.impl.auth.DigestSchemeFactory; |
| import org.apache.http.impl.auth.NTLMSchemeFactory; |
| import org.apache.http.impl.client.BasicAuthCache; |
| import org.apache.http.impl.client.BasicCredentialsProvider; |
| import org.apache.http.impl.client.CloseableHttpClient; |
| import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; |
| import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy; |
| import org.apache.http.impl.client.HttpClientBuilder; |
| import org.apache.http.impl.client.StandardHttpRequestRetryHandler; |
| import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; |
| import org.apache.http.message.BasicHeader; |
| import org.apache.http.protocol.HTTP; |
| import org.apache.http.util.EntityUtils; |
| import org.apache.maven.wagon.InputData; |
| import org.apache.maven.wagon.OutputData; |
| import org.apache.maven.wagon.PathUtils; |
| import org.apache.maven.wagon.ResourceDoesNotExistException; |
| import org.apache.maven.wagon.StreamWagon; |
| import org.apache.maven.wagon.TransferFailedException; |
| import org.apache.maven.wagon.Wagon; |
| import org.apache.maven.wagon.authorization.AuthorizationException; |
| import org.apache.maven.wagon.events.TransferEvent; |
| import org.apache.maven.wagon.proxy.ProxyInfo; |
| import org.apache.maven.wagon.repository.Repository; |
| import org.apache.maven.wagon.resource.Resource; |
| |
| import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatAuthorizationMessage; |
| import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage; |
| import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferDebugMessage; |
| import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatTransferFailedMessage; |
| |
| /** |
| * @author <a href="michal.maczka@dimatics.com">Michal Maczka</a> |
| * @author <a href="mailto:james@atlassian.com">James William Dumay</a> |
| */ |
| public abstract class AbstractHttpClientWagon extends StreamWagon { |
| final class WagonHttpEntity extends AbstractHttpEntity { |
| private final Resource resource; |
| |
| private final Wagon wagon; |
| |
| private InputStream stream; |
| |
| private File source; |
| |
| private long length = -1; |
| |
| private boolean repeatable; |
| |
| private WagonHttpEntity(final InputStream stream, final Resource resource, final Wagon wagon, final File source) |
| throws TransferFailedException { |
| if (source != null) { |
| this.source = source; |
| this.repeatable = true; |
| } else { |
| this.stream = stream; |
| this.repeatable = false; |
| } |
| this.resource = resource; |
| this.length = resource == null ? -1 : resource.getContentLength(); |
| |
| this.wagon = wagon; |
| } |
| |
| public Resource getResource() { |
| return resource; |
| } |
| |
| public Wagon getWagon() { |
| return wagon; |
| } |
| |
| public InputStream getContent() throws IOException, IllegalStateException { |
| if (this.source != null) { |
| return new FileInputStream(this.source); |
| } |
| return stream; |
| } |
| |
| public File getSource() { |
| return source; |
| } |
| |
| public long getContentLength() { |
| return length; |
| } |
| |
| public boolean isRepeatable() { |
| return repeatable; |
| } |
| |
| public void writeTo(final OutputStream output) throws IOException { |
| if (output == null) { |
| throw new NullPointerException("output cannot be null"); |
| } |
| TransferEvent transferEvent = |
| new TransferEvent(wagon, resource, TransferEvent.TRANSFER_PROGRESS, TransferEvent.REQUEST_PUT); |
| transferEvent.setTimestamp(System.currentTimeMillis()); |
| |
| try (ReadableByteChannel input = (this.source != null) |
| ? new RandomAccessFile(this.source, "r").getChannel() |
| : Channels.newChannel(stream)) { |
| ByteBuffer buffer = ByteBuffer.allocate(getBufferCapacityForTransfer(this.length)); |
| int halfBufferCapacity = buffer.capacity() / 2; |
| |
| long remaining = this.length < 0L ? Long.MAX_VALUE : this.length; |
| while (remaining > 0L) { |
| int read = input.read(buffer); |
| if (read == -1) { |
| // EOF, but some data has not been written yet. |
| if (((Buffer) buffer).position() != 0) { |
| ((Buffer) buffer).flip(); |
| fireTransferProgress(transferEvent, buffer.array(), ((Buffer) buffer).limit()); |
| output.write(buffer.array(), 0, ((Buffer) buffer).limit()); |
| ((Buffer) buffer).clear(); |
| } |
| |
| break; |
| } |
| |
| // Prevent minichunking/fragmentation: when less than half the buffer is utilized, |
| // read some more bytes before writing and firing progress. |
| if (((Buffer) buffer).position() < halfBufferCapacity) { |
| continue; |
| } |
| |
| ((Buffer) buffer).flip(); |
| fireTransferProgress(transferEvent, buffer.array(), ((Buffer) buffer).limit()); |
| output.write(buffer.array(), 0, ((Buffer) buffer).limit()); |
| remaining -= ((Buffer) buffer).limit(); |
| ((Buffer) buffer).clear(); |
| } |
| output.flush(); |
| } |
| } |
| |
| public boolean isStreaming() { |
| return true; |
| } |
| } |
| |
| private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT"); |
| |
| /** |
| * use http(s) connection pool mechanism. |
| * <b>enabled by default</b> |
| */ |
| private static boolean persistentPool = Boolean.valueOf(System.getProperty("maven.wagon.http.pool", "true")); |
| |
| /** |
| * skip failure on certificate validity checks. |
| * <b>disabled by default</b> |
| */ |
| private static final boolean SSL_INSECURE = |
| Boolean.valueOf(System.getProperty("maven.wagon.http.ssl.insecure", "false")); |
| |
| /** |
| * if using sslInsecure, certificate date issues will be ignored |
| * <b>disabled by default</b> |
| */ |
| private static final boolean IGNORE_SSL_VALIDITY_DATES = |
| Boolean.valueOf(System.getProperty("maven.wagon.http.ssl.ignore.validity.dates", "false")); |
| |
| /** |
| * If enabled, ssl hostname verifier does not check hostname. Disable this will use a browser compat hostname |
| * verifier <b>disabled by default</b> |
| */ |
| private static final boolean SSL_ALLOW_ALL = |
| Boolean.valueOf(System.getProperty("maven.wagon.http.ssl.allowall", "false")); |
| |
| /** |
| * Maximum concurrent connections per distinct route. |
| * <b>20 by default</b> |
| */ |
| private static final int MAX_CONN_PER_ROUTE = |
| Integer.parseInt(System.getProperty("maven.wagon.httpconnectionManager.maxPerRoute", "20")); |
| |
| /** |
| * Maximum concurrent connections in total. |
| * <b>40 by default</b> |
| */ |
| private static final int MAX_CONN_TOTAL = |
| Integer.parseInt(System.getProperty("maven.wagon.httpconnectionManager.maxTotal", "40")); |
| |
| /** |
| * Time to live in seconds for an HTTP connection. After that time, the connection will be dropped. |
| * Intermediates tend to drop connections after some idle period. Set to -1 to maintain connections |
| * indefinitely. This value defaults to 300 seconds. |
| * |
| * @since 3.2 |
| */ |
| private static final long CONN_TTL = Long.getLong("maven.wagon.httpconnectionManager.ttlSeconds", 300L); |
| |
| /** |
| * Internal connection manager |
| */ |
| private static HttpClientConnectionManager httpClientConnectionManager = createConnManager(); |
| |
| /** |
| * See RFC6585 |
| */ |
| protected static final int SC_TOO_MANY_REQUESTS = 429; |
| |
| /** |
| * For exponential backoff. |
| */ |
| |
| /** |
| * Initial seconds to back off when a HTTP 429 received. |
| * Subsequent 429 responses result in exponental backoff. |
| * <b>5 by default</b> |
| * |
| * @since 2.7 |
| */ |
| private int initialBackoffSeconds = |
| Integer.parseInt(System.getProperty("maven.wagon.httpconnectionManager.backoffSeconds", "5")); |
| |
| /** |
| * The maximum amount of time we want to back off in the case of |
| * repeated HTTP 429 response codes. |
| * |
| * @since 2.7 |
| */ |
| private static final int MAX_BACKOFF_WAIT_SECONDS = |
| Integer.parseInt(System.getProperty("maven.wagon.httpconnectionManager.maxBackoffSeconds", "180")); |
| |
| protected int backoff(int wait, String url) throws InterruptedException, TransferFailedException { |
| TimeUnit.SECONDS.sleep(wait); |
| int nextWait = wait * 2; |
| if (nextWait >= getMaxBackoffWaitSeconds()) { |
| throw new TransferFailedException( |
| formatTransferFailedMessage(url, SC_TOO_MANY_REQUESTS, null, getProxyInfo())); |
| } |
| return nextWait; |
| } |
| |
| @SuppressWarnings("checkstyle:linelength") |
| private static PoolingHttpClientConnectionManager createConnManager() { |
| |
| String sslProtocolsStr = System.getProperty("https.protocols"); |
| String cipherSuitesStr = System.getProperty("https.cipherSuites"); |
| String[] sslProtocols = sslProtocolsStr != null ? sslProtocolsStr.split(" *, *") : null; |
| String[] cipherSuites = cipherSuitesStr != null ? cipherSuitesStr.split(" *, *") : null; |
| |
| SSLConnectionSocketFactory sslConnectionSocketFactory; |
| if (SSL_INSECURE) { |
| try { |
| SSLContext sslContext = new SSLContextBuilder() |
| .useSSL() |
| .loadTrustMaterial(null, new RelaxedTrustStrategy(IGNORE_SSL_VALIDITY_DATES)) |
| .build(); |
| sslConnectionSocketFactory = new SSLConnectionSocketFactory( |
| sslContext, |
| sslProtocols, |
| cipherSuites, |
| SSL_ALLOW_ALL |
| ? SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER |
| : SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); |
| } catch (Exception ex) { |
| throw new SSLInitializationException(ex.getMessage(), ex); |
| } |
| } else { |
| sslConnectionSocketFactory = new SSLConnectionSocketFactory( |
| HttpsURLConnection.getDefaultSSLSocketFactory(), |
| sslProtocols, |
| cipherSuites, |
| SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); |
| } |
| |
| Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() |
| .register("http", PlainConnectionSocketFactory.INSTANCE) |
| .register("https", sslConnectionSocketFactory) |
| .build(); |
| |
| PoolingHttpClientConnectionManager connManager = |
| new PoolingHttpClientConnectionManager(registry, null, null, null, CONN_TTL, TimeUnit.SECONDS); |
| if (persistentPool) { |
| connManager.setDefaultMaxPerRoute(MAX_CONN_PER_ROUTE); |
| connManager.setMaxTotal(MAX_CONN_TOTAL); |
| } else { |
| connManager.setMaxTotal(1); |
| } |
| return connManager; |
| } |
| |
| /** |
| * The type of the retry handler, defaults to {@code standard}. |
| * Values can be {@link default DefaultHttpRequestRetryHandler}, |
| * or {@link standard StandardHttpRequestRetryHandler}, |
| * or a fully qualified name class with a no-arg. |
| * |
| * @since 3.2 |
| */ |
| private static final String RETRY_HANDLER_CLASS = |
| System.getProperty("maven.wagon.http.retryHandler.class", "standard"); |
| |
| /** |
| * Whether or not methods that have successfully sent their request will be retried, |
| * defaults to {@code false}. |
| * Note: only used for default and standard retry handlers. |
| * |
| * @since 3.2 |
| */ |
| private static final boolean RETRY_HANDLER_REQUEST_SENT_ENABLED = |
| Boolean.getBoolean("maven.wagon.http.retryHandler.requestSentEnabled"); |
| |
| /** |
| * Number of retries for the retry handler, defaults to 3. |
| * Note: only used for default and standard retry handlers. |
| * |
| * @since 3.2 |
| */ |
| private static final int RETRY_HANDLER_COUNT = Integer.getInteger("maven.wagon.http.retryHandler.count", 3); |
| |
| /** |
| * Comma-separated list of non-retryable exception classes. |
| * Note: only used for default retry handler. |
| * |
| * @since 3.2 |
| */ |
| private static final String RETRY_HANDLER_EXCEPTIONS = |
| System.getProperty("maven.wagon.http.retryHandler.nonRetryableClasses"); |
| |
| private static HttpRequestRetryHandler createRetryHandler() { |
| switch (RETRY_HANDLER_CLASS) { |
| case "default": |
| if (RETRY_HANDLER_EXCEPTIONS == null || RETRY_HANDLER_EXCEPTIONS.isEmpty()) { |
| return new DefaultHttpRequestRetryHandler(RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED); |
| } |
| return new DefaultHttpRequestRetryHandler( |
| RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED, getNonRetryableExceptions()) {}; |
| case "standard": |
| return new StandardHttpRequestRetryHandler(RETRY_HANDLER_COUNT, RETRY_HANDLER_REQUEST_SENT_ENABLED); |
| default: |
| try { |
| final ClassLoader classLoader = AbstractHttpClientWagon.class.getClassLoader(); |
| return HttpRequestRetryHandler.class.cast(classLoader |
| .loadClass(RETRY_HANDLER_CLASS) |
| .getConstructor() |
| .newInstance()); |
| } catch (final Exception e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| } |
| |
| /** |
| * The type of the serviceUnavailableRetryStrategy, defaults to {@code none}. |
| * Values can be {@link default DefaultServiceUnavailableRetryStrategy}, |
| * or {@link standard StandardServiceUnavailableRetryStrategy}, or |
| * a fully qualified name class with a no-arg or none to not use a ServiceUnavailableRetryStrategy. |
| */ |
| private static final String SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS = |
| System.getProperty("maven.wagon.http.serviceUnavailableRetryStrategy.class", "none"); |
| |
| /** |
| * Interval in milliseconds between retries when using a serviceUnavailableRetryStrategy. |
| * <b>1000 by default</b> |
| */ |
| private static final int SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL = |
| Integer.getInteger("maven.wagon.http.serviceUnavailableRetryStrategy.retryInterval", 1000); |
| |
| /** |
| * Maximum number of retries when using a serviceUnavailableRetryStrategy. |
| * <b>5 by default</b> |
| */ |
| private static final int SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES = |
| Integer.getInteger("maven.wagon.http.serviceUnavailableRetryStrategy.maxRetries", 5); |
| |
| private static ServiceUnavailableRetryStrategy createServiceUnavailableRetryStrategy() { |
| switch (SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS) { |
| case "none": |
| return null; |
| case "default": |
| return new DefaultServiceUnavailableRetryStrategy( |
| SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES, |
| SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL); |
| case "standard": |
| return new StandardServiceUnavailableRetryStrategy( |
| SERVICE_UNAVAILABLE_RETRY_STRATEGY_MAX_RETRIES, |
| SERVICE_UNAVAILABLE_RETRY_STRATEGY_RETRY_INTERVAL); |
| default: |
| try { |
| final ClassLoader classLoader = AbstractHttpClientWagon.class.getClassLoader(); |
| return ServiceUnavailableRetryStrategy.class.cast(classLoader |
| .loadClass(SERVICE_UNAVAILABLE_RETRY_STRATEGY_CLASS) |
| .getConstructor() |
| .newInstance()); |
| } catch (final Exception e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| } |
| |
| private static Registry<AuthSchemeProvider> createAuthSchemeRegistry() { |
| return RegistryBuilder.<AuthSchemeProvider>create() |
| .register(AuthSchemes.BASIC, new BasicSchemeFactory(StandardCharsets.UTF_8)) |
| .register(AuthSchemes.DIGEST, new DigestSchemeFactory(StandardCharsets.UTF_8)) |
| .register(AuthSchemes.NTLM, new NTLMSchemeFactory()) |
| .build(); |
| } |
| |
| private static Collection<Class<? extends IOException>> getNonRetryableExceptions() { |
| final List<Class<? extends IOException>> exceptions = new ArrayList<>(); |
| final ClassLoader loader = AbstractHttpClientWagon.class.getClassLoader(); |
| for (final String ex : RETRY_HANDLER_EXCEPTIONS.split(",")) { |
| try { |
| exceptions.add((Class<? extends IOException>) loader.loadClass(ex)); |
| } catch (final ClassNotFoundException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| return exceptions; |
| } |
| |
| private static CloseableHttpClient httpClient = createClient(); |
| |
| private static CloseableHttpClient createClient() { |
| return HttpClientBuilder.create() // |
| .useSystemProperties() // |
| .disableConnectionState() // |
| .setConnectionManager(httpClientConnectionManager) // |
| .setRetryHandler(createRetryHandler()) |
| .setServiceUnavailableRetryStrategy(createServiceUnavailableRetryStrategy()) |
| .setDefaultAuthSchemeRegistry(createAuthSchemeRegistry()) |
| .setRedirectStrategy(new WagonRedirectStrategy()) |
| .build(); |
| } |
| |
| private CredentialsProvider credentialsProvider; |
| |
| private AuthCache authCache; |
| |
| private Closeable closeable; |
| |
| /** |
| * @plexus.configuration |
| * @deprecated Use httpConfiguration instead. |
| */ |
| private Properties httpHeaders; |
| |
| /** |
| * @since 1.0-beta-6 |
| */ |
| private HttpConfiguration httpConfiguration; |
| |
| /** |
| * Basic auth scope overrides |
| * @since 2.8 |
| */ |
| private BasicAuthScope basicAuth; |
| |
| /** |
| * Proxy basic auth scope overrides |
| * @since 2.8 |
| */ |
| private BasicAuthScope proxyAuth; |
| |
| public void openConnectionInternal() { |
| repository.setUrl(getURL(repository)); |
| |
| credentialsProvider = new BasicCredentialsProvider(); |
| authCache = new BasicAuthCache(); |
| |
| if (authenticationInfo != null) { |
| |
| String username = authenticationInfo.getUserName(); |
| String password = authenticationInfo.getPassword(); |
| |
| if ((username != null && !username.isEmpty()) && (password != null && !password.isEmpty())) { |
| Credentials creds = new UsernamePasswordCredentials(username, password); |
| |
| AuthScope targetScope = getBasicAuthScope() |
| .getScope(getRepository().getHost(), getRepository().getPort()); |
| credentialsProvider.setCredentials(targetScope, creds); |
| } |
| } |
| |
| ProxyInfo proxyInfo = |
| getProxyInfo(getRepository().getProtocol(), getRepository().getHost()); |
| if (proxyInfo != null) { |
| String proxyUsername = proxyInfo.getUserName(); |
| String proxyPassword = proxyInfo.getPassword(); |
| String proxyHost = proxyInfo.getHost(); |
| String proxyNtlmHost = proxyInfo.getNtlmHost(); |
| String proxyNtlmDomain = proxyInfo.getNtlmDomain(); |
| if (proxyHost != null) { |
| if (proxyUsername != null && proxyPassword != null) { |
| Credentials creds; |
| if (proxyNtlmHost != null || proxyNtlmDomain != null) { |
| creds = new NTCredentials(proxyUsername, proxyPassword, proxyNtlmHost, proxyNtlmDomain); |
| } else { |
| creds = new UsernamePasswordCredentials(proxyUsername, proxyPassword); |
| } |
| |
| AuthScope proxyScope = getProxyBasicAuthScope().getScope(proxyHost, proxyInfo.getPort()); |
| credentialsProvider.setCredentials(proxyScope, creds); |
| } |
| } |
| } |
| } |
| |
| public void closeConnection() { |
| if (!persistentPool) { |
| httpClientConnectionManager.closeIdleConnections(0, TimeUnit.MILLISECONDS); |
| } |
| |
| if (authCache != null) { |
| authCache.clear(); |
| authCache = null; |
| } |
| |
| if (credentialsProvider != null) { |
| credentialsProvider.clear(); |
| credentialsProvider = null; |
| } |
| } |
| |
| public static CloseableHttpClient getHttpClient() { |
| return httpClient; |
| } |
| |
| public static void setPersistentPool(boolean persistent) { |
| persistentPool = persistent; |
| } |
| |
| public static void setPoolingHttpClientConnectionManager( |
| PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) { |
| httpClientConnectionManager = poolingHttpClientConnectionManager; |
| httpClient = createClient(); |
| } |
| |
| public void put(File source, String resourceName) |
| throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { |
| Resource resource = new Resource(resourceName); |
| |
| firePutInitiated(resource, source); |
| |
| resource.setContentLength(source.length()); |
| |
| resource.setLastModified(source.lastModified()); |
| |
| put(null, resource, source); |
| } |
| |
| public void putFromStream(final InputStream stream, String destination, long contentLength, long lastModified) |
| throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { |
| Resource resource = new Resource(destination); |
| |
| firePutInitiated(resource, null); |
| |
| resource.setContentLength(contentLength); |
| |
| resource.setLastModified(lastModified); |
| |
| put(stream, resource, null); |
| } |
| |
| private void put(final InputStream stream, Resource resource, File source) |
| throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { |
| put(resource, source, new WagonHttpEntity(stream, resource, this, source)); |
| } |
| |
| private void put(Resource resource, File source, HttpEntity httpEntity) |
| throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { |
| put(resource, source, httpEntity, buildUrl(resource)); |
| } |
| |
| /** |
| * Builds a complete URL string from the repository URL and the relative path of the resource passed. |
| * |
| * @param resource the resource to extract the relative path from. |
| * @return the complete URL |
| */ |
| private String buildUrl(Resource resource) { |
| return buildUrl(resource.getName()); |
| } |
| |
| /** |
| * Builds a complete URL string from the repository URL and the relative path of the resource passed. |
| * |
| * @param resourceName the resourcerelative path |
| * @return the complete URL |
| */ |
| private String buildUrl(String resourceName) { |
| return EncodingUtil.encodeURLToString(getRepository().getUrl(), resourceName); |
| } |
| |
| private void put(Resource resource, File source, HttpEntity httpEntity, String url) |
| throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { |
| put(getInitialBackoffSeconds(), resource, source, httpEntity, url); |
| } |
| |
| private void put(int wait, Resource resource, File source, HttpEntity httpEntity, String url) |
| throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { |
| |
| // Parent directories need to be created before posting |
| try { |
| mkdirs(PathUtils.dirname(resource.getName())); |
| } catch (HttpException he) { |
| fireTransferError(resource, he, TransferEvent.REQUEST_PUT); |
| } catch (IOException e) { |
| fireTransferError(resource, e, TransferEvent.REQUEST_PUT); |
| } |
| |
| // preemptive for put |
| // TODO: is it a good idea, though? 'Expect-continue' handshake would serve much better |
| |
| // FIXME Perform only when preemptive has been configured |
| Repository repo = getRepository(); |
| HttpHost targetHost = new HttpHost(repo.getHost(), repo.getPort(), repo.getProtocol()); |
| AuthScope targetScope = getBasicAuthScope().getScope(targetHost); |
| |
| if (credentialsProvider.getCredentials(targetScope) != null) { |
| BasicScheme targetAuth = new BasicScheme(StandardCharsets.UTF_8); |
| authCache.put(targetHost, targetAuth); |
| } |
| |
| HttpPut putMethod = new HttpPut(url); |
| |
| firePutStarted(resource, source); |
| |
| try { |
| putMethod.setEntity(httpEntity); |
| |
| CloseableHttpResponse response = execute(putMethod); |
| try { |
| fireTransferDebug(formatTransferDebugMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| int statusCode = response.getStatusLine().getStatusCode(); |
| |
| // Check that we didn't run out of retries. |
| switch (statusCode) { |
| // Success Codes |
| case HttpStatus.SC_OK: // 200 |
| case HttpStatus.SC_CREATED: // 201 |
| case HttpStatus.SC_ACCEPTED: // 202 |
| case HttpStatus.SC_NO_CONTENT: // 204 |
| break; |
| |
| // TODO Move 401/407 to AuthenticationException after WAGON-587 |
| case HttpStatus.SC_FORBIDDEN: |
| case HttpStatus.SC_UNAUTHORIZED: |
| case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| fireSessionConnectionRefused(); |
| throw new AuthorizationException(formatAuthorizationMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| |
| case HttpStatus.SC_NOT_FOUND: |
| case HttpStatus.SC_GONE: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| throw new ResourceDoesNotExistException(formatResourceDoesNotExistMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| |
| case SC_TOO_MANY_REQUESTS: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| put(backoff(wait, url), resource, source, httpEntity, url); |
| break; |
| // add more entries here |
| default: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| TransferFailedException e = new TransferFailedException(formatTransferFailedMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| fireTransferError(resource, e, TransferEvent.REQUEST_PUT); |
| throw e; |
| } |
| |
| firePutCompleted(resource, source); |
| |
| EntityUtils.consume(response.getEntity()); |
| } finally { |
| response.close(); |
| } |
| } catch (IOException | HttpException | InterruptedException e) { |
| fireTransferError(resource, e, TransferEvent.REQUEST_PUT); |
| |
| throw new TransferFailedException(formatTransferFailedMessage(url, getProxyInfo()), e); |
| } |
| } |
| |
| protected void mkdirs(String dirname) throws HttpException, IOException { |
| // nothing to do |
| } |
| |
| public boolean resourceExists(String resourceName) throws TransferFailedException, AuthorizationException { |
| return resourceExists(getInitialBackoffSeconds(), resourceName); |
| } |
| |
| private boolean resourceExists(int wait, String resourceName) |
| throws TransferFailedException, AuthorizationException { |
| String url = buildUrl(resourceName); |
| HttpHead headMethod = new HttpHead(url); |
| try { |
| CloseableHttpResponse response = execute(headMethod); |
| try { |
| int statusCode = response.getStatusLine().getStatusCode(); |
| boolean result; |
| switch (statusCode) { |
| case HttpStatus.SC_OK: |
| result = true; |
| break; |
| case HttpStatus.SC_NOT_MODIFIED: |
| result = true; |
| break; |
| |
| // TODO Move 401/407 to AuthenticationException after WAGON-587 |
| case HttpStatus.SC_FORBIDDEN: |
| case HttpStatus.SC_UNAUTHORIZED: |
| case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: |
| throw new AuthorizationException(formatAuthorizationMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| |
| case HttpStatus.SC_NOT_FOUND: |
| case HttpStatus.SC_GONE: |
| result = false; |
| break; |
| |
| case SC_TOO_MANY_REQUESTS: |
| return resourceExists(backoff(wait, resourceName), resourceName); |
| |
| // add more entries here |
| default: |
| throw new TransferFailedException(formatTransferFailedMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| } |
| |
| return result; |
| } finally { |
| response.close(); |
| } |
| } catch (IOException | HttpException | InterruptedException e) { |
| throw new TransferFailedException(formatTransferFailedMessage(url, getProxyInfo()), e); |
| } |
| } |
| |
| protected CloseableHttpResponse execute(HttpUriRequest httpMethod) throws HttpException, IOException { |
| setHeaders(httpMethod); |
| String userAgent = getUserAgent(httpMethod); |
| if (userAgent != null) { |
| httpMethod.setHeader(HTTP.USER_AGENT, userAgent); |
| } |
| |
| RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); |
| // WAGON-273: default the cookie-policy to browser compatible |
| requestConfigBuilder.setCookieSpec(CookieSpecs.BROWSER_COMPATIBILITY); |
| |
| Repository repo = getRepository(); |
| ProxyInfo proxyInfo = getProxyInfo(repo.getProtocol(), repo.getHost()); |
| if (proxyInfo != null) { |
| HttpHost proxy = new HttpHost(proxyInfo.getHost(), proxyInfo.getPort()); |
| requestConfigBuilder.setProxy(proxy); |
| } |
| |
| requestConfigBuilder.setConnectTimeout(getTimeout()); |
| requestConfigBuilder.setSocketTimeout(getReadTimeout()); |
| // We don't apply this to MKCOL because RFC 7231 says that this will not work without a body |
| // and our MKCOL requests don't have a body. They will logically behave like GET. |
| if (httpMethod instanceof HttpPut) { |
| requestConfigBuilder.setExpectContinueEnabled(true); |
| } |
| |
| HttpMethodConfiguration config = |
| httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration(httpMethod); |
| if (config != null) { |
| ConfigurationUtils.copyConfig(config, requestConfigBuilder); |
| } |
| |
| HttpClientContext localContext = HttpClientContext.create(); |
| localContext.setCredentialsProvider(credentialsProvider); |
| localContext.setAuthCache(authCache); |
| localContext.setRequestConfig(requestConfigBuilder.build()); |
| |
| if (config != null && config.isUsePreemptive()) { |
| HttpHost targetHost = new HttpHost(repo.getHost(), repo.getPort(), repo.getProtocol()); |
| AuthScope targetScope = getBasicAuthScope().getScope(targetHost); |
| |
| if (credentialsProvider.getCredentials(targetScope) != null) { |
| BasicScheme targetAuth = new BasicScheme(StandardCharsets.UTF_8); |
| authCache.put(targetHost, targetAuth); |
| } |
| } |
| |
| if (proxyInfo != null) { |
| if (proxyInfo.getHost() != null) { |
| HttpHost proxyHost = new HttpHost(proxyInfo.getHost(), proxyInfo.getPort()); |
| AuthScope proxyScope = getProxyBasicAuthScope().getScope(proxyHost); |
| |
| if (credentialsProvider.getCredentials(proxyScope) != null) { |
| /* This is extremely ugly because we need to set challengeState to PROXY, but |
| * the constructor is deprecated. Alternatively, we could subclass BasicScheme |
| * to ProxyBasicScheme and set the state internally in the constructor. |
| */ |
| BasicScheme proxyAuth = new BasicScheme(ChallengeState.PROXY); |
| authCache.put(proxyHost, proxyAuth); |
| } |
| } |
| } |
| |
| return httpClient.execute(httpMethod, localContext); |
| } |
| |
| public void setHeaders(HttpUriRequest method) { |
| HttpMethodConfiguration config = |
| httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration(method); |
| if (config == null || config.isUseDefaultHeaders()) { |
| // TODO: merge with the other headers and have some better defaults, unify with lightweight headers |
| method.addHeader("Cache-control", "no-cache"); |
| method.addHeader("Pragma", "no-cache"); |
| } |
| |
| if (httpHeaders != null) { |
| for (Map.Entry<Object, Object> entry : httpHeaders.entrySet()) { |
| method.setHeader((String) entry.getKey(), (String) entry.getValue()); |
| } |
| } |
| |
| Header[] headers = config == null ? null : config.asRequestHeaders(); |
| if (headers != null) { |
| for (Header header : headers) { |
| method.setHeader(header); |
| } |
| } |
| |
| Header userAgentHeader = method.getFirstHeader(HTTP.USER_AGENT); |
| if (userAgentHeader == null) { |
| String userAgent = getUserAgent(method); |
| if (userAgent != null) { |
| method.setHeader(HTTP.USER_AGENT, userAgent); |
| } |
| } |
| } |
| |
| protected String getUserAgent(HttpUriRequest method) { |
| if (httpHeaders != null) { |
| String value = (String) httpHeaders.get(HTTP.USER_AGENT); |
| if (value != null) { |
| return value; |
| } |
| } |
| HttpMethodConfiguration config = |
| httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration(method); |
| |
| if (config != null) { |
| return (String) config.getHeaders().get(HTTP.USER_AGENT); |
| } |
| return null; |
| } |
| |
| /** |
| * getUrl |
| * Implementors can override this to remove unwanted parts of the url such as role-hints |
| * |
| * @param repository |
| * @return |
| */ |
| protected String getURL(Repository repository) { |
| return repository.getUrl(); |
| } |
| |
| public HttpConfiguration getHttpConfiguration() { |
| return httpConfiguration; |
| } |
| |
| public void setHttpConfiguration(HttpConfiguration httpConfiguration) { |
| this.httpConfiguration = httpConfiguration; |
| } |
| |
| /** |
| * Get the override values for standard HttpClient AuthScope |
| * |
| * @return the basicAuth |
| */ |
| public BasicAuthScope getBasicAuthScope() { |
| if (basicAuth == null) { |
| basicAuth = new BasicAuthScope(); |
| } |
| return basicAuth; |
| } |
| |
| /** |
| * Set the override values for standard HttpClient AuthScope |
| * |
| * @param basicAuth the AuthScope to set |
| */ |
| public void setBasicAuthScope(BasicAuthScope basicAuth) { |
| this.basicAuth = basicAuth; |
| } |
| |
| /** |
| * Get the override values for proxy HttpClient AuthScope |
| * |
| * @return the proxyAuth |
| */ |
| public BasicAuthScope getProxyBasicAuthScope() { |
| if (proxyAuth == null) { |
| proxyAuth = new BasicAuthScope(); |
| } |
| return proxyAuth; |
| } |
| |
| /** |
| * Set the override values for proxy HttpClient AuthScope |
| * |
| * @param proxyAuth the AuthScope to set |
| */ |
| public void setProxyBasicAuthScope(BasicAuthScope proxyAuth) { |
| this.proxyAuth = proxyAuth; |
| } |
| |
| public void fillInputData(InputData inputData) |
| throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { |
| fillInputData(getInitialBackoffSeconds(), inputData); |
| } |
| |
| private void fillInputData(int wait, InputData inputData) |
| throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { |
| Resource resource = inputData.getResource(); |
| |
| String url = buildUrl(resource); |
| HttpGet getMethod = new HttpGet(url); |
| long timestamp = resource.getLastModified(); |
| if (timestamp > 0) { |
| SimpleDateFormat fmt = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss zzz", Locale.US); |
| fmt.setTimeZone(GMT_TIME_ZONE); |
| Header hdr = new BasicHeader("If-Modified-Since", fmt.format(new Date(timestamp))); |
| fireTransferDebug("sending ==> " + hdr + "(" + timestamp + ")"); |
| getMethod.addHeader(hdr); |
| } |
| |
| try { |
| CloseableHttpResponse response = execute(getMethod); |
| closeable = response; |
| |
| fireTransferDebug(formatTransferDebugMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| int statusCode = response.getStatusLine().getStatusCode(); |
| |
| switch (statusCode) { |
| case HttpStatus.SC_OK: |
| break; |
| |
| case HttpStatus.SC_NOT_MODIFIED: |
| // return, leaving last modified set to original value so getIfNewer should return unmodified |
| return; |
| |
| // TODO Move 401/407 to AuthenticationException after WAGON-587 |
| case HttpStatus.SC_FORBIDDEN: |
| case HttpStatus.SC_UNAUTHORIZED: |
| case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| fireSessionConnectionRefused(); |
| throw new AuthorizationException(formatAuthorizationMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| |
| case HttpStatus.SC_NOT_FOUND: |
| case HttpStatus.SC_GONE: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| throw new ResourceDoesNotExistException(formatResourceDoesNotExistMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| |
| case SC_TOO_MANY_REQUESTS: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| fillInputData(backoff(wait, url), inputData); |
| break; |
| |
| // add more entries here |
| default: |
| EntityUtils.consumeQuietly(response.getEntity()); |
| cleanupGetTransfer(resource); |
| TransferFailedException e = new TransferFailedException(formatTransferFailedMessage( |
| url, |
| response.getStatusLine().getStatusCode(), |
| response.getStatusLine().getReasonPhrase(), |
| getProxyInfo())); |
| fireTransferError(resource, e, TransferEvent.REQUEST_GET); |
| throw e; |
| } |
| |
| Header contentLengthHeader = response.getFirstHeader("Content-Length"); |
| |
| if (contentLengthHeader != null) { |
| try { |
| long contentLength = Long.parseLong(contentLengthHeader.getValue()); |
| |
| resource.setContentLength(contentLength); |
| } catch (NumberFormatException e) { |
| fireTransferDebug( |
| "error parsing content length header '" + contentLengthHeader.getValue() + "' " + e); |
| } |
| } |
| |
| Header lastModifiedHeader = response.getFirstHeader("Last-Modified"); |
| if (lastModifiedHeader != null) { |
| Date lastModified = DateUtils.parseDate(lastModifiedHeader.getValue()); |
| if (lastModified != null) { |
| resource.setLastModified(lastModified.getTime()); |
| fireTransferDebug( |
| "last-modified = " + lastModifiedHeader.getValue() + " (" + lastModified.getTime() + ")"); |
| } |
| } |
| |
| HttpEntity entity = response.getEntity(); |
| if (entity != null) { |
| inputData.setInputStream(entity.getContent()); |
| } |
| } catch (IOException | HttpException | InterruptedException e) { |
| fireTransferError(resource, e, TransferEvent.REQUEST_GET); |
| |
| throw new TransferFailedException(formatTransferFailedMessage(url, getProxyInfo()), e); |
| } |
| } |
| |
| protected void cleanupGetTransfer(Resource resource) { |
| if (closeable != null) { |
| try { |
| closeable.close(); |
| } catch (IOException ignore) { |
| // ignore |
| } |
| } |
| } |
| |
| @Override |
| public void putFromStream(InputStream stream, String destination) |
| throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException { |
| putFromStream(stream, destination, -1, -1); |
| } |
| |
| @Override |
| protected void putFromStream(InputStream stream, Resource resource) |
| throws TransferFailedException, AuthorizationException, ResourceDoesNotExistException { |
| putFromStream(stream, resource.getName(), -1, -1); |
| } |
| |
| public Properties getHttpHeaders() { |
| return httpHeaders; |
| } |
| |
| public void setHttpHeaders(Properties httpHeaders) { |
| this.httpHeaders = httpHeaders; |
| } |
| |
| @Override |
| public void fillOutputData(OutputData outputData) throws TransferFailedException { |
| // no needed in this implementation but throw an Exception if used |
| throw new IllegalStateException("this wagon http client must not use fillOutputData"); |
| } |
| |
| protected CredentialsProvider getCredentialsProvider() { |
| return credentialsProvider; |
| } |
| |
| protected AuthCache getAuthCache() { |
| return authCache; |
| } |
| |
| public int getInitialBackoffSeconds() { |
| return initialBackoffSeconds; |
| } |
| |
| public void setInitialBackoffSeconds(int initialBackoffSeconds) { |
| this.initialBackoffSeconds = initialBackoffSeconds; |
| } |
| |
| public static int getMaxBackoffWaitSeconds() { |
| return MAX_BACKOFF_WAIT_SECONDS; |
| } |
| } |