blob: 6ee3040c421a9d165ec970fea2d623eca0846be4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.apache.brooklyn.util.http;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.crypto.SslTrustUtils;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.net.URLParamEncoder;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeSocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
/**
* A utility tool for HTTP operations.
*/
public class HttpTool {
private static final Logger LOG = LoggerFactory.getLogger(HttpTool.class);
static final ExecutorService executor = Executors.newCachedThreadPool();
/**
* Connects to the given url and returns the connection.
* Caller should {@code connection.getInputStream().close()} the result of this
* (especially if they are making heavy use of this method).
*/
public static URLConnection connectToUrl(String u) throws Exception {
final URL url = new URL(u);
final AtomicReference<Exception> exception = new AtomicReference<Exception>();
// sometimes openConnection hangs, so run in background
Future<URLConnection> f = executor.submit(new Callable<URLConnection>() {
public URLConnection call() {
try {
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
});
URLConnection connection = url.openConnection();
TrustingSslSocketFactory.configure(connection);
connection.connect();
connection.getContentLength(); // Make sure the connection is made.
return connection;
} catch (Exception e) {
exception.set(e);
LOG.debug("Error connecting to url "+url+" (propagating): "+e, e);
}
return null;
}
});
try {
URLConnection result = null;
try {
result = f.get(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw e;
} catch (Exception e) {
LOG.debug("Error connecting to url "+url+", probably timed out (rethrowing): "+e);
throw new IllegalStateException("Connect to URL not complete within 60 seconds, for url "+url+": "+e);
}
if (exception.get() != null) {
LOG.debug("Error connecting to url "+url+", thread caller of "+exception, new Throwable("source of rethrown error "+exception));
throw exception.get();
} else {
return result;
}
} finally {
f.cancel(true);
}
}
public static int getHttpStatusCode(String url) throws Exception {
URLConnection connection = connectToUrl(url);
long startTime = System.currentTimeMillis();
int status = ((HttpURLConnection) connection).getResponseCode();
// read fully if possible, then close everything, trying to prevent cached threads at server
consumeAndCloseQuietly((HttpURLConnection) connection);
if (LOG.isDebugEnabled())
LOG.debug("connection to {} ({}ms) gives {}", new Object[] { url, (System.currentTimeMillis()-startTime), status });
return status;
}
public static String getContent(String url) {
try {
return Streams.readFullyStringAndClose(SslTrustUtils.trustAll(new URL(url).openConnection()).getInputStream());
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
public static String getErrorContent(String url) {
try {
HttpURLConnection connection = (HttpURLConnection) connectToUrl(url);
long startTime = System.currentTimeMillis();
String err;
int status;
try {
InputStream errStream = connection.getErrorStream();
err = Streams.readFullyStringAndClose(errStream);
status = connection.getResponseCode();
} finally {
closeQuietly(connection);
}
if (LOG.isDebugEnabled())
LOG.debug("read of err {} ({}ms) complete; http code {}", new Object[] { url, Time.makeTimeStringRounded(System.currentTimeMillis() - startTime), status});
return err;
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}
/**
* Consumes the input stream entirely and then cleanly closes the connection.
* Ignores all exceptions completely, not even logging them!
*
* Consuming the stream fully is useful for preventing idle TCP connections.
* @see <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/net/http-keepalive.html">Persistent Connections</a>
*/
public static void consumeAndCloseQuietly(HttpURLConnection connection) {
try { Streams.readFully(connection.getInputStream()); } catch (Exception e) {}
closeQuietly(connection);
}
/**
* Closes all streams of the connection, and disconnects it. Ignores all exceptions completely,
* not even logging them!
*/
public static void closeQuietly(HttpURLConnection connection) {
try { connection.disconnect(); } catch (Exception e) {}
try { connection.getInputStream().close(); } catch (Exception e) {}
try { connection.getOutputStream().close(); } catch (Exception e) {}
try { connection.getErrorStream().close(); } catch (Exception e) {}
}
/** Apache HTTP commons utility for trusting all.
* <p>
* For generic java HTTP usage, see {@link SslTrustUtils#trustAll(java.net.URLConnection)}
* and static constants in the same class. */
public static class TrustAllStrategy implements TrustStrategy {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
}
public static HttpClientBuilder httpClientBuilder() {
return new HttpClientBuilder();
}
// TODO deprecate this and use the new Apache Commons HttpClientBuilder instead
@SuppressWarnings("deprecation")
public static class HttpClientBuilder {
private ClientConnectionManager clientConnectionManager;
private HttpParams httpParams;
private URI uri;
private Integer port;
private Credentials credentials;
private boolean laxRedirect;
private Boolean https;
private SchemeSocketFactory socketFactory;
private ConnectionReuseStrategy reuseStrategy;
private boolean trustAll;
private boolean trustSelfSigned;
public static HttpClientBuilder fromBuilder(HttpClientBuilder other) {
HttpClientBuilder result = httpClientBuilder();
result.clientConnectionManager = other.clientConnectionManager;
result.httpParams = other.httpParams;
result.uri = other.uri;
result.port = other.port;
result.credentials = other.credentials;
result.laxRedirect = other.laxRedirect;
result.https = other.https;
result.socketFactory = other.socketFactory;
result.reuseStrategy = other.reuseStrategy;
result.trustAll = other.trustAll;
result.trustSelfSigned = other.trustSelfSigned;
return result;
}
public HttpClientBuilder clientConnectionManager(ClientConnectionManager val) {
this.clientConnectionManager = checkNotNull(val, "clientConnectionManager");
return this;
}
public HttpClientBuilder httpParams(HttpParams val) {
checkState(httpParams == null, "Must not call httpParams multiple times, or after other methods like connectionTimeout");
this.httpParams = checkNotNull(val, "httpParams");
return this;
}
public HttpClientBuilder connectionTimeout(Duration val) {
if (httpParams == null) httpParams = new BasicHttpParams();
long millis = checkNotNull(val, "connectionTimeout").toMilliseconds();
if (millis > Integer.MAX_VALUE) throw new IllegalStateException("HttpClient only accepts upto max-int millis for connectionTimeout, but given "+val);
HttpConnectionParams.setConnectionTimeout(httpParams, (int) millis);
return this;
}
public HttpClientBuilder socketTimeout(Duration val) {
if (httpParams == null) httpParams = new BasicHttpParams();
long millis = checkNotNull(val, "socketTimeout").toMilliseconds();
if (millis > Integer.MAX_VALUE) throw new IllegalStateException("HttpClient only accepts upto max-int millis for socketTimeout, but given "+val);
HttpConnectionParams.setSoTimeout(httpParams, (int) millis);
return this;
}
public HttpClientBuilder reuseStrategy(ConnectionReuseStrategy val) {
this.reuseStrategy = checkNotNull(val, "reuseStrategy");
return this;
}
public HttpClientBuilder uri(String val) {
return uri(URI.create(checkNotNull(val, "uri")));
}
public HttpClientBuilder uri(URI val) {
this.uri = checkNotNull(val, "uri");
if (https == null) https = ("https".equalsIgnoreCase(uri.getScheme()));
return this;
}
public HttpClientBuilder port(int val) {
this.port = val;
return this;
}
public HttpClientBuilder credentials(Credentials val) {
this.credentials = checkNotNull(val, "credentials");
return this;
}
public HttpClientBuilder credential(Optional<? extends Credentials> val) {
if (val.isPresent()) credentials = val.get();
return this;
}
/** similar to curl --post301 -L` */
public HttpClientBuilder laxRedirect(boolean val) {
this.laxRedirect = val;
return this;
}
public HttpClientBuilder https(boolean val) {
this.https = val;
return this;
}
public HttpClientBuilder socketFactory(SchemeSocketFactory val) {
this.socketFactory = checkNotNull(val, "socketFactory");
return this;
}
public HttpClientBuilder trustAll() {
return trustAll(true);
}
public HttpClientBuilder trustSelfSigned() {
return trustSelfSigned(true);
}
public HttpClientBuilder trustAll(boolean val) {
trustAll = val;
return this;
}
public HttpClientBuilder trustSelfSigned(boolean val) {
this.trustSelfSigned = val;
return this;
}
public HttpClient build() {
final DefaultHttpClient httpClient = new DefaultHttpClient(clientConnectionManager);
httpClient.setParams(httpParams);
// support redirects for POST (similar to `curl --post301 -L`)
// http://stackoverflow.com/questions/3658721/httpclient-4-error-302-how-to-redirect
if (laxRedirect) {
httpClient.setRedirectStrategy(new LaxRedirectStrategy());
}
if (reuseStrategy != null) {
httpClient.setReuseStrategy(reuseStrategy);
}
if (https == Boolean.TRUE || (uri!=null && uri.toString().startsWith("https:"))) {
try {
if (port == null) {
port = (uri != null && uri.getPort() >= 0) ? uri.getPort() : 443;
}
if (socketFactory == null) {
if (trustAll) {
TrustStrategy trustStrategy = new TrustAllStrategy();
X509HostnameVerifier hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
socketFactory = new SSLSocketFactory(trustStrategy, hostnameVerifier);
} else if (trustSelfSigned) {
TrustStrategy trustStrategy = new TrustSelfSignedStrategy();
X509HostnameVerifier hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
socketFactory = new SSLSocketFactory(trustStrategy, hostnameVerifier);
} else {
// Using default https scheme: based on default java truststore, which is pretty strict!
}
}
if (socketFactory != null) {
Scheme sch = new Scheme("https", port, socketFactory);
httpClient.getConnectionManager().getSchemeRegistry().register(sch);
}
} catch (Exception e) {
LOG.warn("Error setting trust for uri {}", uri);
throw Exceptions.propagate(e);
}
}
// Set credentials
if (uri != null && credentials != null) {
String hostname = uri.getHost();
int port = uri.getPort();
httpClient.getCredentialsProvider().setCredentials(new AuthScope(hostname, port), credentials);
}
if (uri==null && credentials!=null) {
LOG.warn("credentials have no effect in builder unless URI for host is specified");
}
return httpClient;
}
}
public static class HttpRequestBuilder<R extends HttpRequestBase> {
private Class<R> requestClass;
private Map<String, String> headers;
private URI uri;
private HttpEntity body;
public HttpRequestBuilder(Class<R> requestClass) {
this.requestClass = requestClass;
this.headers = MutableMap.of();
}
public HttpRequestBuilder<R> uri(URI uri) {
this.uri = uri;
return this;
}
public HttpRequestBuilder<R> headers(Map<String, String> headers) {
if (headers != null) {
this.headers.putAll(headers);
}
return this;
}
public HttpRequestBuilder<R> headers(Multimap<String, String> headers) {
if (headers != null) {
for (Map.Entry<String,String> entry : headers.entries()) {
this.headers.put(entry.getKey(), entry.getValue());
}
}
return this;
}
public HttpRequestBuilder<R> body(byte[] body) {
if (body != null) {
this.body = new ByteArrayEntity(body);
}
return this;
}
public HttpRequestBuilder<R> body(String body) {
if (body != null) {
this.body(body.getBytes(Charset.forName("UTF-8")));
}
return this;
}
public HttpRequestBuilder<R> body(Map<String, String> body) {
if (body != null) {
Collection<NameValuePair> httpParams = new ArrayList<NameValuePair>(body.size());
for (Entry<String, String> param : body.entrySet()) {
httpParams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
this.body = new UrlEncodedFormEntity(httpParams);
}
return this;
}
public R build() {
try {
R request = this.requestClass.newInstance();
request.setURI(this.uri);
for (Map.Entry<String,String> entry : this.headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
if (this.body != null) {
if (request instanceof HttpPost) {
((HttpPost) request).setEntity(this.body);
} else if (request instanceof HttpPut) {
((HttpPut) request).setEntity(this.body);
} else {
throw new Exception(this.requestClass.getSimpleName() + " does not support a request body");
}
}
return request;
} catch (Exception e) {
LOG.warn("Cannot create the HTTP request for uri {}", this.uri);
throw Exceptions.propagate(e);
}
}
}
public static class HttpGetBuilder extends HttpRequestBuilder<HttpGet> {
public HttpGetBuilder(URI uri) {
super(HttpGet.class);
this.uri(uri);
}
}
public static class HttpHeadBuilder extends HttpRequestBuilder<HttpHead> {
public HttpHeadBuilder(URI uri) {
super(HttpHead.class);
this.uri(uri);
}
}
public static class HttpDeleteBuilder extends HttpRequestBuilder<HttpDelete> {
public HttpDeleteBuilder(URI uri) {
super(HttpDelete.class);
this.uri(uri);
}
}
public static class HttpPostBuilder extends HttpRequestBuilder<HttpPost> {
public HttpPostBuilder(URI uri) {
super(HttpPost.class);
this.uri(uri);
}
}
public static class HttpPutBuilder extends HttpRequestBuilder<HttpPut> {
public HttpPutBuilder(URI uri) {
super(HttpPut.class);
this.uri(uri);
}
}
public static HttpToolResponse httpGet(HttpClient httpClient, URI uri, Map<String,String> headers) {
HttpGet req = new HttpGetBuilder(uri).headers(headers).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpGet(HttpClient httpClient, URI uri, Multimap<String,String> headers) {
HttpGet req = new HttpGetBuilder(uri).headers(headers).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpPost(HttpClient httpClient, URI uri, Multimap<String,String> headers, byte[] body) {
HttpPost req = new HttpPostBuilder(uri).headers(headers).body(body).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpPost(HttpClient httpClient, URI uri, Map<String,String> headers, byte[] body) {
HttpPost req = new HttpPostBuilder(uri).headers(headers).body(body).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpPut(HttpClient httpClient, URI uri, Multimap<String, String> headers, byte[] body) {
HttpPut req = new HttpPutBuilder(uri).headers(headers).body(body).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpPut(HttpClient httpClient, URI uri, Map<String, String> headers, byte[] body) {
HttpPut req = new HttpPutBuilder(uri).headers(headers).body(body).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpPost(HttpClient httpClient, URI uri, Map<String,String> headers, Map<String, String> params) {
HttpPost req = new HttpPostBuilder(uri).body(params).headers(headers).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpDelete(HttpClient httpClient, URI uri, Multimap<String,String> headers) {
HttpDelete req = new HttpDeleteBuilder(uri).headers(headers).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpDelete(HttpClient httpClient, URI uri, Map<String,String> headers) {
HttpDelete req = new HttpDeleteBuilder(uri).headers(headers).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpHead(HttpClient httpClient, URI uri, Multimap<String,String> headers) {
HttpHead req = new HttpHeadBuilder(uri).headers(headers).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse httpHead(HttpClient httpClient, URI uri, Map<String,String> headers) {
HttpHead req = new HttpHeadBuilder(uri).headers(headers).build();
return execAndConsume(httpClient, req);
}
public static HttpToolResponse execAndConsume(HttpClient httpClient, HttpUriRequest req) {
long startTime = System.currentTimeMillis();
try {
HttpResponse httpResponse = httpClient.execute(req);
try {
return new HttpToolResponse(httpResponse, startTime);
} finally {
EntityUtils.consume(httpResponse.getEntity());
}
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}
public static boolean isStatusCodeHealthy(int code) { return (code>=200 && code<=299); }
public static String toBasicAuthorizationValue(UsernamePasswordCredentials credentials) {
return "Basic "+Base64.encodeBase64String( (credentials.getUserName()+":"+credentials.getPassword()).getBytes() );
}
public static String encodeUrlParams(Map<?,?> data) {
if (data==null) return "";
Iterable<String> args = Iterables.transform(data.entrySet(),
new Function<Map.Entry<?,?>,String>() {
@Override public String apply(Map.Entry<?,?> entry) {
Object k = entry.getKey();
Object v = entry.getValue();
return URLParamEncoder.encode(Strings.toString(k)) + (v != null ? "=" + URLParamEncoder.encode(Strings.toString(v)) : "");
}
});
return Joiner.on("&").join(args);
}
}