View Javadoc

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 }