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;
20  
21  import static com.google.common.base.Preconditions.checkArgument;
22  import static com.google.common.base.Preconditions.checkState;
23  import static com.google.common.base.Throwables.getCausalChain;
24  import static com.google.common.base.Throwables.propagate;
25  import static com.google.common.collect.Iterables.filter;
26  import static com.google.common.collect.Iterables.get;
27  import static com.google.common.collect.Iterables.size;
28  import static com.google.common.collect.Lists.newArrayList;
29  import static com.google.common.io.ByteStreams.toByteArray;
30  import static com.google.common.io.Closeables.closeQuietly;
31  import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING;
32  import static javax.ws.rs.core.HttpHeaders.CONTENT_LANGUAGE;
33  import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
34  import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
35  import static org.jclouds.util.Patterns.PATTERN_THAT_BREAKS_URI;
36  import static org.jclouds.util.Patterns.URI_PATTERN;
37  
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.net.URI;
41  import java.util.Collection;
42  import java.util.List;
43  import java.util.Map.Entry;
44  import java.util.regex.Matcher;
45  
46  import javax.inject.Named;
47  import javax.inject.Singleton;
48  import javax.ws.rs.core.HttpHeaders;
49  
50  import org.jclouds.Constants;
51  import org.jclouds.crypto.CryptoStreams;
52  import org.jclouds.io.ContentMetadata;
53  import org.jclouds.io.InputSuppliers;
54  import org.jclouds.io.MutableContentMetadata;
55  import org.jclouds.io.Payload;
56  import org.jclouds.io.PayloadEnclosing;
57  import org.jclouds.io.Payloads;
58  import org.jclouds.logging.Logger;
59  import org.jclouds.logging.internal.Wire;
60  import org.jclouds.util.Strings2;
61  
62  import com.google.common.base.Joiner;
63  import com.google.common.base.Predicate;
64  import com.google.common.base.Splitter;
65  import com.google.common.collect.ImmutableMultimap;
66  import com.google.common.collect.ImmutableMultimap.Builder;
67  import com.google.common.collect.Multimap;
68  import com.google.common.collect.SortedSetMultimap;
69  import com.google.common.collect.TreeMultimap;
70  import com.google.inject.Inject;
71  
72  /**
73   * @author Adrian Cole
74   */
75  @Singleton
76  public class HttpUtils {
77  
78     @Inject(optional = true)
79     @Named(Constants.PROPERTY_RELAX_HOSTNAME)
80     private boolean relaxHostname = false;
81  
82     @Inject(optional = true)
83     @Named(Constants.PROPERTY_PROXY_SYSTEM)
84     private boolean systemProxies = System.getProperty("java.net.useSystemProxies") != null ? Boolean
85           .parseBoolean(System.getProperty("java.net.useSystemProxies")) : false;
86  
87     private final int globalMaxConnections;
88     private final int globalMaxConnectionsPerHost;
89     private final int connectionTimeout;
90     private final int soTimeout;
91     @Inject(optional = true)
92     @Named(Constants.PROPERTY_PROXY_HOST)
93     private String proxyHost;
94     @Inject(optional = true)
95     @Named(Constants.PROPERTY_PROXY_PORT)
96     private Integer proxyPort;
97     @Inject(optional = true)
98     @Named(Constants.PROPERTY_PROXY_USER)
99     private String proxyUser;
100    @Inject(optional = true)
101    @Named(Constants.PROPERTY_PROXY_PASSWORD)
102    private String proxyPassword;
103    @Inject(optional = true)
104    @Named(Constants.PROPERTY_TRUST_ALL_CERTS)
105    private boolean trustAllCerts;
106 
107    @Inject
108    public HttpUtils(@Named(Constants.PROPERTY_CONNECTION_TIMEOUT) int connectionTimeout,
109          @Named(Constants.PROPERTY_SO_TIMEOUT) int soTimeout,
110          @Named(Constants.PROPERTY_MAX_CONNECTIONS_PER_CONTEXT) int globalMaxConnections,
111          @Named(Constants.PROPERTY_MAX_CONNECTIONS_PER_HOST) int globalMaxConnectionsPerHost) {
112       this.soTimeout = soTimeout;
113       this.connectionTimeout = connectionTimeout;
114       this.globalMaxConnections = globalMaxConnections;
115       this.globalMaxConnectionsPerHost = globalMaxConnectionsPerHost;
116    }
117 
118    /**
119     * @see org.jclouds.Constants.PROPERTY_PROXY_HOST
120     */
121    public String getProxyHost() {
122       return proxyHost;
123    }
124 
125    /**
126     * @see org.jclouds.Constants.PROPERTY_PROXY_PORT
127     */
128    public Integer getProxyPort() {
129       return proxyPort;
130    }
131 
132    /**
133     * @see org.jclouds.Constants.PROPERTY_PROXY_USER
134     */
135    public String getProxyUser() {
136       return proxyUser;
137    }
138 
139    /**
140     * @see org.jclouds.Constants.PROPERTY_PROXY_PASSWORD
141     */
142    public String getProxyPassword() {
143       return proxyPassword;
144    }
145 
146    public int getSocketOpenTimeout() {
147       return soTimeout;
148    }
149 
150    public int getConnectionTimeout() {
151       return connectionTimeout;
152    }
153 
154    public boolean relaxHostname() {
155       return relaxHostname;
156    }
157 
158    public boolean trustAllCerts() {
159       return trustAllCerts;
160    }
161 
162    public boolean useSystemProxies() {
163       return systemProxies;
164    }
165 
166    public int getMaxConnections() {
167       return globalMaxConnections;
168    }
169 
170    public int getMaxConnectionsPerHost() {
171       return globalMaxConnectionsPerHost;
172    }
173 
174    /**
175     * keys to the map are only used for socket information, not path. In this case, you should
176     * remove any path or query details from the URI.
177     */
178    public static URI createBaseEndpointFor(URI endpoint) {
179       if (endpoint.getPort() == -1) {
180          return URI.create(String.format("%s://%s", endpoint.getScheme(), endpoint.getHost()));
181       } else {
182          return URI.create(String.format("%s://%s:%d", endpoint.getScheme(), endpoint.getHost(), endpoint.getPort()));
183       }
184    }
185 
186    public static Multimap<String, String> getContentHeadersFromMetadata(ContentMetadata md) {
187        Builder<String, String> builder = ImmutableMultimap.builder();
188       if (md.getContentType() != null)
189          builder.put(HttpHeaders.CONTENT_TYPE, md.getContentType());
190       if (md.getContentDisposition() != null)
191          builder.put("Content-Disposition", md.getContentDisposition());
192       if (md.getContentEncoding() != null)
193          builder.put(HttpHeaders.CONTENT_ENCODING, md.getContentEncoding());
194       if (md.getContentLanguage() != null)
195          builder.put(HttpHeaders.CONTENT_LANGUAGE, md.getContentLanguage());
196       if (md.getContentLength() != null)
197          builder.put(HttpHeaders.CONTENT_LENGTH, md.getContentLength() + "");
198       if (md.getContentMD5() != null)
199          builder.put("Content-MD5", CryptoStreams.base64(md.getContentMD5()));
200       return builder.build();
201    }
202 
203    public static byte[] toByteArrayOrNull(PayloadEnclosing response) {
204       if (response.getPayload() != null) {
205          InputStream input = response.getPayload().getInput();
206          try {
207             return toByteArray(input);
208          } catch (IOException e) {
209             propagate(e);
210          } finally {
211             closeQuietly(input);
212          }
213       }
214       return null;
215    }
216 
217    /**
218     * Content stream may need to be read. However, we should always close the http stream.
219     * 
220     * @throws IOException
221     */
222    public static byte[] closeClientButKeepContentStream(PayloadEnclosing response) {
223       byte[] returnVal = toByteArrayOrNull(response);
224       if (returnVal != null && !response.getPayload().isRepeatable()) {
225          Payload newPayload = Payloads.newByteArrayPayload(returnVal);
226          MutableContentMetadata fromMd = response.getPayload().getContentMetadata();
227          MutableContentMetadata toMd = newPayload.getContentMetadata();
228          copy(fromMd, toMd);
229          response.setPayload(newPayload);
230       }
231       return returnVal;
232    }
233 
234    public static void copy(ContentMetadata fromMd, MutableContentMetadata toMd) {
235       toMd.setContentLength(fromMd.getContentLength());
236       toMd.setContentMD5(fromMd.getContentMD5());
237       toMd.setContentType(fromMd.getContentType());
238       toMd.setContentDisposition(fromMd.getContentDisposition());
239       toMd.setContentEncoding(fromMd.getContentEncoding());
240       toMd.setContentLanguage(fromMd.getContentLanguage());
241    }
242 
243    public static URI parseEndPoint(String hostHeader) {
244       URI redirectURI = URI.create(hostHeader);
245       String scheme = redirectURI.getScheme();
246 
247       checkState(redirectURI.getScheme().startsWith("http"),
248             String.format("header %s didn't parse an http scheme: [%s]", hostHeader, scheme));
249       int port = redirectURI.getPort() > 0 ? redirectURI.getPort() : redirectURI.getScheme().equals("https") ? 443 : 80;
250       String host = redirectURI.getHost();
251       checkState(host.indexOf('/') == -1,
252             String.format("header %s didn't parse an http host correctly: [%s]", hostHeader, host));
253       URI endPoint = URI.create(String.format("%s://%s:%d", scheme, host, port));
254       return endPoint;
255    }
256 
257    public static URI replaceHostInEndPoint(URI endPoint, String host) {
258       return URI.create(endPoint.toString().replace(endPoint.getHost(), host));
259    }
260 
261    /**
262     * Used to extract the URI and authentication data from a String. Note that the java URI class
263     * breaks, if there are special characters like '/' present. Otherwise, we wouldn't need this
264     * class, and we could simply use URI.create("uri").getUserData(); Also, URI breaks if there are
265     * curly braces.
266     * 
267     */
268    public static URI createUri(String uriPath) {
269       List<String> onQuery = newArrayList(Splitter.on('?').split(uriPath));
270       if (onQuery.size() == 2) {
271          onQuery.add(Strings2.urlEncode(onQuery.remove(1), '=', '&'));
272          uriPath = Joiner.on('?').join(onQuery);
273       }
274       if (uriPath.indexOf('@') != 1) {
275          List<String> parts = newArrayList(Splitter.on('@').split(uriPath));
276          String path = parts.remove(parts.size() - 1);
277          if (parts.size() > 1) {
278             parts = newArrayList(Strings2.urlEncode(Joiner.on('@').join(parts), '/', ':'));
279          }
280          parts.add(Strings2.urlEncode(path, '/', ':'));
281          uriPath = Joiner.on('@').join(parts);
282       } else {
283          List<String> parts = newArrayList(Splitter.on('/').split(uriPath));
284          String path = parts.remove(parts.size() - 1);
285          parts.add(Strings2.urlEncode(path, ':'));
286          uriPath = Joiner.on('/').join(parts);
287       }
288 
289       if (PATTERN_THAT_BREAKS_URI.matcher(uriPath).matches()) {
290          // Compile and use regular expression
291          Matcher matcher = URI_PATTERN.matcher(uriPath);
292          if (matcher.find()) {
293             String scheme = matcher.group(1);
294             String rest = matcher.group(4);
295             String identity = matcher.group(2);
296             String key = matcher.group(3);
297             return URI.create(String.format("%s://%s:%s@%s", scheme, Strings2.urlEncode(identity),
298                   Strings2.urlEncode(key), rest));
299          } else {
300             throw new IllegalArgumentException("bad syntax");
301          }
302       } else {
303          return URI.create(uriPath);
304       }
305    }
306 
307    public void logRequest(Logger logger, HttpRequest request, String prefix) {
308       if (logger.isDebugEnabled()) {
309          logger.debug("%s %s", prefix, request.getRequestLine().toString());
310          logMessage(logger, request, prefix);
311       }
312    }
313 
314    private void logMessage(Logger logger, HttpMessage message, String prefix) {
315       for (Entry<String, String> header : message.getHeaders().entries()) {
316          if (header.getKey() != null)
317             logger.debug("%s %s: %s", prefix, header.getKey(), header.getValue());
318       }
319       if (message.getPayload() != null) {
320          if (message.getPayload().getContentMetadata().getContentType() != null)
321             logger.debug("%s %s: %s", prefix, CONTENT_TYPE, message.getPayload().getContentMetadata().getContentType());
322          if (message.getPayload().getContentMetadata().getContentLength() != null)
323             logger.debug("%s %s: %s", prefix, CONTENT_LENGTH, message.getPayload().getContentMetadata()
324                   .getContentLength());
325          if (message.getPayload().getContentMetadata().getContentMD5() != null)
326             try {
327                logger.debug(
328                      "%s %s: %s",
329                      prefix,
330                      "Content-MD5",
331                      CryptoStreams.base64Encode(InputSuppliers.of(message.getPayload().getContentMetadata()
332                            .getContentMD5())));
333             } catch (IOException e) {
334                logger.warn(e, " error getting md5 for %s", message);
335             }
336          if (message.getPayload().getContentMetadata().getContentDisposition() != null)
337             logger.debug("%s %s: %s", prefix, "Content-Disposition", message.getPayload().getContentMetadata()
338                   .getContentDisposition());
339          if (message.getPayload().getContentMetadata().getContentEncoding() != null)
340             logger.debug("%s %s: %s", prefix, CONTENT_ENCODING, message.getPayload().getContentMetadata()
341                   .getContentEncoding());
342          if (message.getPayload().getContentMetadata().getContentLanguage() != null)
343             logger.debug("%s %s: %s", prefix, CONTENT_LANGUAGE, message.getPayload().getContentMetadata()
344                   .getContentLanguage());
345       }
346    }
347 
348    public void logResponse(Logger logger, HttpResponse response, String prefix) {
349       if (logger.isDebugEnabled()) {
350          logger.debug("%s %s", prefix, response.getStatusLine().toString());
351          logMessage(logger, response, prefix);
352       }
353    }
354 
355    public static String sortAndConcatHeadersIntoString(Multimap<String, String> headers) {
356       StringBuffer buffer = new StringBuffer();
357       SortedSetMultimap<String, String> sortedMap = TreeMultimap.create();
358       sortedMap.putAll(headers);
359       for (Entry<String, String> header : sortedMap.entries()) {
360          if (header.getKey() != null)
361             buffer.append(String.format("%s: %s\n", header.getKey(), header.getValue()));
362       }
363       return buffer.toString();
364    }
365 
366    public void checkRequestHasRequiredProperties(HttpRequest message) {
367       checkArgument(
368             message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_TYPE) == null,
369             "configuration error please use request.getPayload().getContentMetadata().setContentType(value) as opposed to adding a content type header: "
370                   + message);
371       checkArgument(
372             message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_LENGTH) == null,
373             "configuration error please use request.getPayload().getContentMetadata().setContentLength(value) as opposed to adding a content length header: "
374                   + message);
375       checkArgument(
376             message.getPayload() == null || message.getPayload().getContentMetadata().getContentLength() != null
377                   || "chunked".equalsIgnoreCase(message.getFirstHeaderOrNull("Transfer-Encoding")),
378             "either chunked encoding must be set on the http request or contentlength set on the payload: " + message);
379       checkArgument(
380             message.getPayload() == null || message.getFirstHeaderOrNull("Content-MD5") == null,
381             "configuration error please use request.getPayload().getContentMetadata().setContentMD5(value) as opposed to adding a content md5 header: "
382                   + message);
383       checkArgument(
384             message.getPayload() == null || message.getFirstHeaderOrNull("Content-Disposition") == null,
385             "configuration error please use request.getPayload().getContentMetadata().setContentDisposition(value) as opposed to adding a content disposition header: "
386                   + message);
387       checkArgument(
388             message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_ENCODING) == null,
389             "configuration error please use request.getPayload().getContentMetadata().setContentEncoding(value) as opposed to adding a content encoding header: "
390                   + message);
391       checkArgument(
392             message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_LANGUAGE) == null,
393             "configuration error please use request.getPayload().getContentMetadata().setContentLanguage(value) as opposed to adding a content language header: "
394                   + message);
395    }
396 
397    public static void releasePayload(HttpMessage from) {
398       if (from.getPayload() != null)
399          from.getPayload().release();
400    }
401 
402    public String valueOrEmpty(String in) {
403       return in != null ? in : "";
404    }
405 
406    public String valueOrEmpty(byte[] md5) {
407       return md5 != null ? CryptoStreams.base64(md5) : "";
408    }
409 
410    public String valueOrEmpty(Collection<String> collection) {
411       return (collection != null && collection.size() >= 1) ? collection.iterator().next() : "";
412    }
413 
414    public static Long attemptToParseSizeAndRangeFromHeaders(HttpMessage from) throws HttpException {
415       String contentRange = from.getFirstHeaderOrNull("Content-Range");
416       if (contentRange == null && from.getPayload() != null) {
417          return from.getPayload().getContentMetadata().getContentLength();
418       } else if (contentRange != null) {
419          return Long.parseLong(contentRange.substring(contentRange.lastIndexOf('/') + 1));
420       }
421       return null;
422    }
423 
424    public static void checkRequestHasContentLengthOrChunkedEncoding(HttpMessage request, String message) {
425       boolean chunked = "chunked".equals(request.getFirstHeaderOrNull("Transfer-Encoding"));
426       checkArgument(request.getPayload() == null || chunked
427             || request.getPayload().getContentMetadata().getContentLength() != null, message);
428    }
429 
430    public static void wirePayloadIfEnabled(Wire wire, HttpMessage request) {
431       if (request.getPayload() != null && wire.enabled()) {
432          wire.output(request);
433          checkRequestHasContentLengthOrChunkedEncoding(request,
434                "After wiring, the request has neither chunked encoding nor content length: " + request);
435       }
436    }
437 
438    public static <T> T returnValueOnCodeOrNull(Exception from, T value, Predicate<Integer> codePredicate) {
439       Iterable<HttpResponseException> throwables = filter(getCausalChain(from), HttpResponseException.class);
440       if (size(throwables) >= 1 && get(throwables, 0).getResponse() != null
441             && codePredicate.apply(get(throwables, 0).getResponse().getStatusCode())) {
442          return value;
443       }
444       return null;
445    }
446 }