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 | } |