| /** |
| * Licensed to jclouds, Inc. (jclouds) under one or more |
| * contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. jclouds 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.jclouds.http.internal; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Throwables.propagate; |
| import static com.google.common.collect.Iterables.getLast; |
| import static com.google.common.io.ByteStreams.toByteArray; |
| import static com.google.common.io.Closeables.closeQuietly; |
| import static org.jclouds.io.Payloads.newInputStreamPayload; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.reflect.Field; |
| import java.net.Authenticator; |
| import java.net.HttpURLConnection; |
| import java.net.InetSocketAddress; |
| import java.net.PasswordAuthentication; |
| import java.net.ProtocolException; |
| import java.net.Proxy; |
| import java.net.ProxySelector; |
| import java.net.SocketAddress; |
| import java.net.URL; |
| import java.util.concurrent.ExecutorService; |
| |
| import javax.annotation.Resource; |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| import javax.inject.Singleton; |
| import javax.net.ssl.HostnameVerifier; |
| import javax.net.ssl.HttpsURLConnection; |
| import javax.net.ssl.SSLContext; |
| import javax.ws.rs.core.HttpHeaders; |
| |
| import org.jclouds.Constants; |
| import org.jclouds.JcloudsVersion; |
| import org.jclouds.crypto.CryptoStreams; |
| import org.jclouds.http.HttpCommandExecutorService; |
| import org.jclouds.http.HttpRequest; |
| import org.jclouds.http.HttpResponse; |
| import org.jclouds.http.HttpUtils; |
| import org.jclouds.http.IOExceptionRetryHandler; |
| import org.jclouds.http.handlers.DelegatingErrorHandler; |
| import org.jclouds.http.handlers.DelegatingRetryHandler; |
| import org.jclouds.io.MutableContentMetadata; |
| import org.jclouds.io.Payload; |
| import org.jclouds.logging.Logger; |
| import org.jclouds.rest.internal.RestAnnotationProcessor; |
| |
| import com.google.common.base.Supplier; |
| import com.google.common.collect.ImmutableMultimap; |
| import com.google.common.collect.ImmutableMultimap.Builder; |
| import com.google.common.io.CountingOutputStream; |
| |
| /** |
| * Basic implementation of a {@link HttpCommandExecutorService}. |
| * |
| * @author Adrian Cole |
| */ |
| @Singleton |
| public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorService<HttpURLConnection> { |
| |
| public static final String USER_AGENT = String.format("jclouds/%s java/%s", JcloudsVersion.get(), System |
| .getProperty("java.version")); |
| |
| @Resource |
| protected Logger logger = Logger.NULL; |
| private final Supplier<SSLContext> untrustedSSLContextProvider; |
| private final HostnameVerifier verifier; |
| private final Field methodField; |
| |
| @Inject |
| public JavaUrlHttpCommandExecutorService(HttpUtils utils, |
| @Named(Constants.PROPERTY_IO_WORKER_THREADS) ExecutorService ioWorkerExecutor, |
| DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler, |
| DelegatingErrorHandler errorHandler, HttpWire wire, @Named("untrusted") HostnameVerifier verifier, |
| @Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider) throws SecurityException, |
| NoSuchFieldException { |
| super(utils, ioWorkerExecutor, retryHandler, ioRetryHandler, errorHandler, wire); |
| if (utils.getMaxConnections() > 0) |
| System.setProperty("http.maxConnections", String.valueOf(checkNotNull(utils, "utils").getMaxConnections())); |
| this.untrustedSSLContextProvider = checkNotNull(untrustedSSLContextProvider, "untrustedSSLContextProvider"); |
| this.verifier = checkNotNull(verifier, "verifier"); |
| this.methodField = HttpURLConnection.class.getDeclaredField("method"); |
| methodField.setAccessible(true); |
| } |
| |
| @Override |
| protected HttpResponse invoke(HttpURLConnection connection) throws IOException, InterruptedException { |
| HttpResponse.Builder builder = HttpResponse.builder(); |
| InputStream in = null; |
| try { |
| in = consumeOnClose(connection.getInputStream()); |
| } catch (IOException e) { |
| in = bufferAndCloseStream(connection.getErrorStream()); |
| } catch (RuntimeException e) { |
| closeQuietly(in); |
| propagate(e); |
| assert false : "should have propagated exception"; |
| } |
| |
| int responseCode = connection.getResponseCode(); |
| if (responseCode == 204) { |
| closeQuietly(in); |
| in = null; |
| } |
| builder.statusCode(responseCode); |
| builder.message(connection.getResponseMessage()); |
| |
| Builder<String, String> headerBuilder = ImmutableMultimap.<String, String> builder(); |
| for (String header : connection.getHeaderFields().keySet()) { |
| // HTTP message comes back as a header without a key |
| if (header != null) |
| headerBuilder.putAll(header, connection.getHeaderFields().get(header)); |
| } |
| ImmutableMultimap<String, String> headers = headerBuilder.build(); |
| Payload payload = in != null ? newInputStreamPayload(in) : null; |
| if (payload != null) { |
| payload.getContentMetadata().setPropertiesFromHttpHeaders(headers); |
| builder.payload(payload); |
| } |
| builder.headers(RestAnnotationProcessor.filterOutContentHeaders(headers)); |
| return builder.build(); |
| } |
| |
| private InputStream bufferAndCloseStream(InputStream inputStream) throws IOException { |
| InputStream in = null; |
| try { |
| if (inputStream != null) { |
| in = new ByteArrayInputStream(toByteArray(inputStream)); |
| } |
| } finally { |
| closeQuietly(inputStream); |
| } |
| return in; |
| } |
| |
| @Override |
| protected HttpURLConnection convert(HttpRequest request) throws IOException, InterruptedException { |
| boolean chunked = "chunked".equals(request.getFirstHeaderOrNull("Transfer-Encoding")); |
| URL url = request.getEndpoint().toURL(); |
| |
| HttpURLConnection connection; |
| |
| if (utils.useSystemProxies()) { |
| System.setProperty("java.net.useSystemProxies", "true"); |
| Iterable<Proxy> proxies = ProxySelector.getDefault().select(request.getEndpoint()); |
| Proxy proxy = getLast(proxies); |
| connection = (HttpURLConnection) url.openConnection(proxy); |
| } else if (utils.getProxyHost() != null) { |
| SocketAddress addr = new InetSocketAddress(utils.getProxyHost(), utils.getProxyPort()); |
| Proxy proxy = new Proxy(Proxy.Type.HTTP, addr); |
| Authenticator authenticator = new Authenticator() { |
| public PasswordAuthentication getPasswordAuthentication() { |
| return (new PasswordAuthentication(utils.getProxyUser(), utils.getProxyPassword().toCharArray())); |
| } |
| }; |
| Authenticator.setDefault(authenticator); |
| connection = (HttpURLConnection) url.openConnection(proxy); |
| } else { |
| connection = (HttpURLConnection) url.openConnection(); |
| } |
| if (connection instanceof HttpsURLConnection) { |
| HttpsURLConnection sslCon = (HttpsURLConnection) connection; |
| if (utils.relaxHostname()) |
| sslCon.setHostnameVerifier(verifier); |
| if (utils.trustAllCerts()) |
| sslCon.setSSLSocketFactory(untrustedSSLContextProvider.get().getSocketFactory()); |
| } |
| if (utils.getConnectionTimeout() > 0) { |
| connection.setConnectTimeout(utils.getConnectionTimeout()); |
| } |
| if (utils.getSocketOpenTimeout() > 0) { |
| connection.setReadTimeout(utils.getSocketOpenTimeout()); |
| } |
| connection.setDoOutput(true); |
| connection.setAllowUserInteraction(false); |
| // do not follow redirects since https redirects don't work properly |
| // ex. Caused by: java.io.IOException: HTTPS hostname wrong: should be |
| // <adriancole.s3int0.s3-external-3.amazonaws.com> |
| connection.setInstanceFollowRedirects(false); |
| try { |
| connection.setRequestMethod(request.getMethod()); |
| } catch (ProtocolException e) { |
| try { |
| methodField.set(connection, request.getMethod()); |
| } catch (Exception e1) { |
| logger.error(e, "could not set request method: ", request.getMethod()); |
| propagate(e1); |
| } |
| } |
| |
| for (String header : request.getHeaders().keys()) { |
| for (String value : request.getHeaders().get(header)) { |
| connection.setRequestProperty(header, value); |
| } |
| } |
| |
| String host = request.getEndpoint().getHost(); |
| if(request.getEndpoint().getPort() != -1) { |
| host += ":" + request.getEndpoint().getPort(); |
| } |
| connection.setRequestProperty(HttpHeaders.HOST, host); |
| connection.setRequestProperty(HttpHeaders.USER_AGENT, USER_AGENT); |
| |
| if (request.getPayload() != null) { |
| MutableContentMetadata md = request.getPayload().getContentMetadata(); |
| if (md.getContentMD5() != null) |
| connection.setRequestProperty("Content-MD5", CryptoStreams.base64(md.getContentMD5())); |
| if (md.getContentType() != null) |
| connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, md.getContentType()); |
| if (md.getContentDisposition() != null) |
| connection.setRequestProperty("Content-Disposition", md.getContentDisposition()); |
| if (md.getContentEncoding() != null) |
| connection.setRequestProperty("Content-Encoding", md.getContentEncoding()); |
| if (md.getContentLanguage() != null) |
| connection.setRequestProperty("Content-Language", md.getContentLanguage()); |
| if (chunked) { |
| connection.setChunkedStreamingMode(8196); |
| } else { |
| Long length = checkNotNull(md.getContentLength(), "payload.getContentLength"); |
| connection.setRequestProperty(HttpHeaders.CONTENT_LENGTH, length.toString()); |
| // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6755625 |
| checkArgument(length < Integer.MAX_VALUE, |
| "JDK 1.6 does not support >2GB chunks. Use chunked encoding, if possible."); |
| connection.setFixedLengthStreamingMode(length.intValue()); |
| if (length.intValue() > 0) { |
| connection.setRequestProperty("Expect", "100-continue"); |
| } |
| } |
| CountingOutputStream out = new CountingOutputStream(connection.getOutputStream()); |
| try { |
| request.getPayload().writeTo(out); |
| } catch (IOException e) { |
| throw new RuntimeException(String.format("error after writing %d/%s bytes to %s", out.getCount(), md |
| .getContentLength(), request.getRequestLine()), e); |
| } |
| } else { |
| connection.setRequestProperty(HttpHeaders.CONTENT_LENGTH, "0"); |
| // for some reason POST/PUT undoes the content length header above. |
| if (connection.getRequestMethod().equals("POST") || connection.getRequestMethod().equals("PUT")) |
| connection.setFixedLengthStreamingMode(0); |
| } |
| return connection; |
| |
| } |
| |
| /** |
| * Only disconnect if there is no content, as disconnecting will throw away unconsumed content. |
| */ |
| @Override |
| protected void cleanup(HttpURLConnection connection) { |
| if (connection != null && connection.getContentLength() == 0) |
| connection.disconnect(); |
| } |
| |
| } |