| 1 | /** |
| 2 | * Licensed to jclouds, Inc. (jclouds) under one or more |
| 3 | * contributor license agreements. See the NOTICE file |
| 4 | * distributed with this work for additional information |
| 5 | * regarding copyright ownership. jclouds licenses this file |
| 6 | * to you under the Apache License, Version 2.0 (the |
| 7 | * "License"); you may not use this file except in compliance |
| 8 | * with the License. You may obtain a copy of the License at |
| 9 | * |
| 10 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | * |
| 12 | * Unless required by applicable law or agreed to in writing, |
| 13 | * software distributed under the License is distributed on an |
| 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | * KIND, either express or implied. See the License for the |
| 16 | * specific language governing permissions and limitations |
| 17 | * under the License. |
| 18 | */ |
| 19 | package org.jclouds.http.internal; |
| 20 | |
| 21 | import static com.google.common.base.Preconditions.checkArgument; |
| 22 | import static com.google.common.base.Preconditions.checkNotNull; |
| 23 | import static com.google.common.base.Throwables.propagate; |
| 24 | import static com.google.common.collect.Iterables.getLast; |
| 25 | import static com.google.common.io.ByteStreams.toByteArray; |
| 26 | import static com.google.common.io.Closeables.closeQuietly; |
| 27 | import static org.jclouds.io.Payloads.newInputStreamPayload; |
| 28 | |
| 29 | import java.io.ByteArrayInputStream; |
| 30 | import java.io.IOException; |
| 31 | import java.io.InputStream; |
| 32 | import java.lang.reflect.Field; |
| 33 | import java.net.Authenticator; |
| 34 | import java.net.HttpURLConnection; |
| 35 | import java.net.InetSocketAddress; |
| 36 | import java.net.PasswordAuthentication; |
| 37 | import java.net.ProtocolException; |
| 38 | import java.net.Proxy; |
| 39 | import java.net.ProxySelector; |
| 40 | import java.net.SocketAddress; |
| 41 | import java.net.URL; |
| 42 | import java.util.concurrent.ExecutorService; |
| 43 | |
| 44 | import javax.annotation.Resource; |
| 45 | import javax.inject.Inject; |
| 46 | import javax.inject.Named; |
| 47 | import javax.inject.Singleton; |
| 48 | import javax.net.ssl.HostnameVerifier; |
| 49 | import javax.net.ssl.HttpsURLConnection; |
| 50 | import javax.net.ssl.SSLContext; |
| 51 | import javax.ws.rs.core.HttpHeaders; |
| 52 | |
| 53 | import org.jclouds.Constants; |
| 54 | import org.jclouds.crypto.CryptoStreams; |
| 55 | import org.jclouds.http.HttpCommandExecutorService; |
| 56 | import org.jclouds.http.HttpRequest; |
| 57 | import org.jclouds.http.HttpResponse; |
| 58 | import org.jclouds.http.HttpUtils; |
| 59 | import org.jclouds.http.IOExceptionRetryHandler; |
| 60 | import org.jclouds.http.handlers.DelegatingErrorHandler; |
| 61 | import org.jclouds.http.handlers.DelegatingRetryHandler; |
| 62 | import org.jclouds.io.MutableContentMetadata; |
| 63 | import org.jclouds.io.Payload; |
| 64 | import org.jclouds.logging.Logger; |
| 65 | import org.jclouds.rest.internal.RestAnnotationProcessor; |
| 66 | |
| 67 | import com.google.common.base.Supplier; |
| 68 | import com.google.common.collect.ImmutableMultimap; |
| 69 | import com.google.common.collect.ImmutableMultimap.Builder; |
| 70 | import com.google.common.io.CountingOutputStream; |
| 71 | |
| 72 | /** |
| 73 | * Basic implementation of a {@link HttpCommandExecutorService}. |
| 74 | * |
| 75 | * @author Adrian Cole |
| 76 | */ |
| 77 | @Singleton |
| 78 | public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorService<HttpURLConnection> { |
| 79 | |
| 80 | public static final String USER_AGENT = "jclouds/1.0 java/" + System.getProperty("java.version"); |
| 81 | @Resource |
| 82 | protected Logger logger = Logger.NULL; |
| 83 | private final Supplier<SSLContext> untrustedSSLContextProvider; |
| 84 | private final HostnameVerifier verifier; |
| 85 | private final Field methodField; |
| 86 | |
| 87 | @Inject |
| 88 | public JavaUrlHttpCommandExecutorService(HttpUtils utils, |
| 89 | @Named(Constants.PROPERTY_IO_WORKER_THREADS) ExecutorService ioWorkerExecutor, |
| 90 | DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler, |
| 91 | DelegatingErrorHandler errorHandler, HttpWire wire, @Named("untrusted") HostnameVerifier verifier, |
| 92 | @Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider) throws SecurityException, |
| 93 | NoSuchFieldException { |
| 94 | super(utils, ioWorkerExecutor, retryHandler, ioRetryHandler, errorHandler, wire); |
| 95 | if (utils.getMaxConnections() > 0) |
| 96 | System.setProperty("http.maxConnections", String.valueOf(checkNotNull(utils, "utils").getMaxConnections())); |
| 97 | this.untrustedSSLContextProvider = checkNotNull(untrustedSSLContextProvider, "untrustedSSLContextProvider"); |
| 98 | this.verifier = checkNotNull(verifier, "verifier"); |
| 99 | this.methodField = HttpURLConnection.class.getDeclaredField("method"); |
| 100 | methodField.setAccessible(true); |
| 101 | } |
| 102 | |
| 103 | @Override |
| 104 | protected HttpResponse invoke(HttpURLConnection connection) throws IOException, InterruptedException { |
| 105 | HttpResponse.Builder builder = HttpResponse.builder(); |
| 106 | InputStream in = null; |
| 107 | try { |
| 108 | in = consumeOnClose(connection.getInputStream()); |
| 109 | } catch (IOException e) { |
| 110 | in = bufferAndCloseStream(connection.getErrorStream()); |
| 111 | } catch (RuntimeException e) { |
| 112 | closeQuietly(in); |
| 113 | propagate(e); |
| 114 | assert false : "should have propagated exception"; |
| 115 | } |
| 116 | |
| 117 | int responseCode = connection.getResponseCode(); |
| 118 | if (responseCode == 204) { |
| 119 | closeQuietly(in); |
| 120 | in = null; |
| 121 | } |
| 122 | builder.statusCode(responseCode); |
| 123 | builder.message(connection.getResponseMessage()); |
| 124 | |
| 125 | Builder<String, String> headerBuilder = ImmutableMultimap.<String, String> builder(); |
| 126 | for (String header : connection.getHeaderFields().keySet()) { |
| 127 | // HTTP message comes back as a header without a key |
| 128 | if (header != null) |
| 129 | headerBuilder.putAll(header, connection.getHeaderFields().get(header)); |
| 130 | } |
| 131 | ImmutableMultimap<String, String> headers = headerBuilder.build(); |
| 132 | Payload payload = in != null ? newInputStreamPayload(in) : null; |
| 133 | if (payload != null) { |
| 134 | payload.getContentMetadata().setPropertiesFromHttpHeaders(headers); |
| 135 | builder.payload(payload); |
| 136 | } |
| 137 | builder.headers(RestAnnotationProcessor.filterOutContentHeaders(headers)); |
| 138 | return builder.build(); |
| 139 | } |
| 140 | |
| 141 | private InputStream bufferAndCloseStream(InputStream inputStream) throws IOException { |
| 142 | InputStream in = null; |
| 143 | try { |
| 144 | if (inputStream != null) { |
| 145 | in = new ByteArrayInputStream(toByteArray(inputStream)); |
| 146 | } |
| 147 | } finally { |
| 148 | closeQuietly(inputStream); |
| 149 | } |
| 150 | return in; |
| 151 | } |
| 152 | |
| 153 | @Override |
| 154 | protected HttpURLConnection convert(HttpRequest request) throws IOException, InterruptedException { |
| 155 | boolean chunked = "chunked".equals(request.getFirstHeaderOrNull("Transfer-Encoding")); |
| 156 | URL url = request.getEndpoint().toURL(); |
| 157 | |
| 158 | HttpURLConnection connection; |
| 159 | |
| 160 | if (utils.useSystemProxies()) { |
| 161 | System.setProperty("java.net.useSystemProxies", "true"); |
| 162 | Iterable<Proxy> proxies = ProxySelector.getDefault().select(request.getEndpoint()); |
| 163 | Proxy proxy = getLast(proxies); |
| 164 | connection = (HttpURLConnection) url.openConnection(proxy); |
| 165 | } else if (utils.getProxyHost() != null) { |
| 166 | SocketAddress addr = new InetSocketAddress(utils.getProxyHost(), utils.getProxyPort()); |
| 167 | Proxy proxy = new Proxy(Proxy.Type.HTTP, addr); |
| 168 | Authenticator authenticator = new Authenticator() { |
| 169 | public PasswordAuthentication getPasswordAuthentication() { |
| 170 | return (new PasswordAuthentication(utils.getProxyUser(), utils.getProxyPassword().toCharArray())); |
| 171 | } |
| 172 | }; |
| 173 | Authenticator.setDefault(authenticator); |
| 174 | connection = (HttpURLConnection) url.openConnection(proxy); |
| 175 | } else { |
| 176 | connection = (HttpURLConnection) url.openConnection(); |
| 177 | } |
| 178 | if (connection instanceof HttpsURLConnection) { |
| 179 | HttpsURLConnection sslCon = (HttpsURLConnection) connection; |
| 180 | if (utils.relaxHostname()) |
| 181 | sslCon.setHostnameVerifier(verifier); |
| 182 | if (utils.trustAllCerts()) |
| 183 | sslCon.setSSLSocketFactory(untrustedSSLContextProvider.get().getSocketFactory()); |
| 184 | } |
| 185 | if (utils.getConnectionTimeout() > 0) { |
| 186 | connection.setConnectTimeout(utils.getConnectionTimeout()); |
| 187 | } |
| 188 | if (utils.getSocketOpenTimeout() > 0) { |
| 189 | connection.setReadTimeout(utils.getSocketOpenTimeout()); |
| 190 | } |
| 191 | connection.setDoOutput(true); |
| 192 | connection.setAllowUserInteraction(false); |
| 193 | // do not follow redirects since https redirects don't work properly |
| 194 | // ex. Caused by: java.io.IOException: HTTPS hostname wrong: should be |
| 195 | // <adriancole.s3int0.s3-external-3.amazonaws.com> |
| 196 | connection.setInstanceFollowRedirects(false); |
| 197 | try { |
| 198 | connection.setRequestMethod(request.getMethod()); |
| 199 | } catch (ProtocolException e) { |
| 200 | try { |
| 201 | methodField.set(connection, request.getMethod()); |
| 202 | } catch (Exception e1) { |
| 203 | logger.error(e, "could not set request method: ", request.getMethod()); |
| 204 | propagate(e1); |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | for (String header : request.getHeaders().keys()) { |
| 209 | for (String value : request.getHeaders().get(header)) { |
| 210 | connection.setRequestProperty(header, value); |
| 211 | } |
| 212 | } |
| 213 | connection.setRequestProperty(HttpHeaders.HOST, request.getEndpoint().getHost()); |
| 214 | connection.setRequestProperty(HttpHeaders.USER_AGENT, USER_AGENT); |
| 215 | |
| 216 | if (request.getPayload() != null) { |
| 217 | MutableContentMetadata md = request.getPayload().getContentMetadata(); |
| 218 | if (md.getContentMD5() != null) |
| 219 | connection.setRequestProperty("Content-MD5", CryptoStreams.base64(md.getContentMD5())); |
| 220 | if (md.getContentType() != null) |
| 221 | connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, md.getContentType()); |
| 222 | if (md.getContentDisposition() != null) |
| 223 | connection.setRequestProperty("Content-Disposition", md.getContentDisposition()); |
| 224 | if (md.getContentEncoding() != null) |
| 225 | connection.setRequestProperty("Content-Encoding", md.getContentEncoding()); |
| 226 | if (md.getContentLanguage() != null) |
| 227 | connection.setRequestProperty("Content-Language", md.getContentLanguage()); |
| 228 | if (chunked) { |
| 229 | connection.setChunkedStreamingMode(8196); |
| 230 | } else { |
| 231 | Long length = checkNotNull(md.getContentLength(), "payload.getContentLength"); |
| 232 | connection.setRequestProperty(HttpHeaders.CONTENT_LENGTH, length.toString()); |
| 233 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6755625 |
| 234 | checkArgument(length < Integer.MAX_VALUE, |
| 235 | "JDK 1.6 does not support >2GB chunks. Use chunked encoding, if possible."); |
| 236 | connection.setFixedLengthStreamingMode(length.intValue()); |
| 237 | if (length.intValue() > 0) { |
| 238 | connection.setRequestProperty("Expect", "100-continue"); |
| 239 | } |
| 240 | } |
| 241 | CountingOutputStream out = new CountingOutputStream(connection.getOutputStream()); |
| 242 | try { |
| 243 | request.getPayload().writeTo(out); |
| 244 | } catch (IOException e) { |
| 245 | throw new RuntimeException(String.format("error after writing %d/%s bytes to %s", out.getCount(), md |
| 246 | .getContentLength(), request.getRequestLine()), e); |
| 247 | } |
| 248 | } else { |
| 249 | connection.setRequestProperty(HttpHeaders.CONTENT_LENGTH, "0"); |
| 250 | // for some reason POST/PUT undoes the content length header above. |
| 251 | if (connection.getRequestMethod().equals("POST") || connection.getRequestMethod().equals("PUT")) |
| 252 | connection.setFixedLengthStreamingMode(0); |
| 253 | } |
| 254 | return connection; |
| 255 | |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Only disconnect if there is no content, as disconnecting will throw away unconsumed content. |
| 260 | */ |
| 261 | @Override |
| 262 | protected void cleanup(HttpURLConnection connection) { |
| 263 | if (connection != null && connection.getContentLength() == 0) |
| 264 | connection.disconnect(); |
| 265 | } |
| 266 | |
| 267 | } |