/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.io.net.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpProxy;
import org.eclipse.jetty.client.ProxyConfiguration;
import org.eclipse.jetty.client.ProxyConfiguration.Proxy;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Some common methods to be used in both HTTP-In-Binding and HTTP-Out-Binding
*
* @author Thomas Eichstaedt-Engelen
* @author Kai Kreuzer - Initial contribution and API
* @author Svilen Valkanov - replaced Apache HttpClient with Jetty
*/
public class HttpUtil {
private static Logger logger = LoggerFactory.getLogger(HttpUtil.class);
private static HttpClient client = new HttpClient(new SslContextFactory());
/**
* Executes the given <code>url</code> with the given <code>httpMethod</code>.
* Furthermore the <code>http.proxyXXX</code> System variables are read and
* set into the {@link HttpClient}.
*
* @param httpMethod the HTTP method to use
* @param url the url to execute (in milliseconds)
* @param timeout the socket timeout to wait for data
*
* @return the response body or <code>NULL</code> when the request went wrong
* @throws IOException when the request execution failed, timed out or it was interrupted
*/
public static String executeUrl(String httpMethod, String url, int timeout) throws IOException {
return executeUrl(httpMethod, url, null, null, timeout);
}
/**
* Executes the given <code>url</code> with the given <code>httpMethod</code>.
* Furthermore the <code>http.proxyXXX</code> System variables are read and
* set into the {@link HttpClient}.
*
* @param httpMethod the HTTP method to use
* @param url the url to execute (in milliseconds)
* @param content the content to be send to the given <code>url</code> or <code>null</code> if no content should be
* send.
* @param contentType the content type of the given <code>content</code>
* @param timeout the socket timeout to wait for data
*
* @return the response body or <code>NULL</code> when the request went wrong
* @throws IOException when the request execution failed, timed out or it was interrupted
*/
public static String executeUrl(String httpMethod, String url, InputStream content, String contentType, int timeout)
throws IOException {
return executeUrl(httpMethod, url, null, content, contentType, timeout);
}
/**
* Executes the given <code>url</code> with the given <code>httpMethod</code>.
* Furthermore the <code>http.proxyXXX</code> System variables are read and
* set into the {@link HttpClient}.
*
* @param httpMethod the HTTP method to use
* @param url the url to execute (in milliseconds)
* @param httpHeaders optional http request headers which has to be sent within request
* @param content the content to be send to the given <code>url</code> or <code>null</code> if no content should be
* send.
* @param contentType the content type of the given <code>content</code>
* @param timeout the socket timeout to wait for data
*
* @return the response body or <code>NULL</code> when the request went wrong
* @throws IOException when the request execution failed, timed out or it was interrupted
*/
public static String executeUrl(String httpMethod, String url, Properties httpHeaders, InputStream content,
String contentType, int timeout) throws IOException {
String proxySet = System.getProperty("http.proxySet");
String proxyHost = null;
int proxyPort = 80;
String proxyUser = null;
String proxyPassword = null;
String nonProxyHosts = null;
if ("true".equalsIgnoreCase(proxySet)) {
proxyHost = System.getProperty("http.proxyHost");
String proxyPortString = System.getProperty("http.proxyPort");
if (StringUtils.isNotBlank(proxyPortString)) {
try {
proxyPort = Integer.valueOf(proxyPortString);
} catch (NumberFormatException e) {
logger.warn("'{}' is not a valid proxy port - using port 80 instead");
}
}
proxyUser = System.getProperty("http.proxyUser");
proxyPassword = System.getProperty("http.proxyPassword");
nonProxyHosts = System.getProperty("http.nonProxyHosts");
}
return executeUrl(httpMethod, url, httpHeaders, content, contentType, timeout, proxyHost, proxyPort, proxyUser,
proxyPassword, nonProxyHosts);
}
/**
* Executes the given <code>url</code> with the given <code>httpMethod</code>
*
* @param httpMethod the HTTP method to use
* @param url the url to execute (in milliseconds)
* @param httpHeaders optional HTTP headers which has to be set on request
* @param content the content to be send to the given <code>url</code> or <code>null</code> if no content should be
* send.
* @param contentType the content type of the given <code>content</code>
* @param timeout the socket timeout to wait for data
* @param proxyHost the hostname of the proxy
* @param proxyPort the port of the proxy
* @param proxyUser the username to authenticate with the proxy
* @param proxyPassword the password to authenticate with the proxy
* @param nonProxyHosts the hosts that won't be routed through the proxy
* @return the response body or <code>NULL</code> when the request went wrong
* @throws IOException when the request execution failed, timed out or it was interrupted
*/
public static String executeUrl(String httpMethod, String url, Properties httpHeaders, InputStream content,
String contentType, int timeout, String proxyHost, Integer proxyPort, String proxyUser,
String proxyPassword, String nonProxyHosts) throws IOException {
startHttpClient(client);
HttpProxy proxy = null;
// only configure a proxy if a host is provided
if (StringUtils.isNotBlank(proxyHost) && proxyPort != null && shouldUseProxy(url, nonProxyHosts)) {
AuthenticationStore authStore = client.getAuthenticationStore();
ProxyConfiguration proxyConfig = client.getProxyConfiguration();
List<Proxy> proxies = proxyConfig.getProxies();
proxy = new HttpProxy(proxyHost, proxyPort);
proxies.add(proxy);
// This value is a replacement for any realm
final String anyRealm = "*";
authStore.addAuthentication(new BasicAuthentication(proxy.getURI(), anyRealm, proxyUser, proxyPassword) {
// In version 9.2.12 Jetty HttpClient does not support adding an authentication for any realm. This is a
// workaround until this issue is solved
@Override
public boolean matches(String type, URI uri, String realm) {
realm = anyRealm;
return super.matches(type, uri, realm);
}
});
}
HttpMethod method = HttpUtil.createHttpMethod(httpMethod);
Request request = client.newRequest(url).method(method).timeout(timeout, TimeUnit.MILLISECONDS);
if (httpHeaders != null) {
for (String httpHeaderKey : httpHeaders.stringPropertyNames()) {
request.header(httpHeaderKey, httpHeaders.getProperty(httpHeaderKey));
}
}
// add basic auth header, if url contains user info
try {
URI uri = new URI(url);
if (uri.getUserInfo() != null) {
String[] userInfo = uri.getUserInfo().split(":");
String user = userInfo[0];
String password = userInfo[1];
String basicAuthentication = "Basic " + B64Code.encode(user + ":" + password, StringUtil.__ISO_8859_1);
request.header(HttpHeader.AUTHORIZATION, basicAuthentication);
}
} catch (URISyntaxException e) {
logger.debug("String {} can not be parsed as URI reference", url);
}
// add content if a valid method is given ...
if (content != null && (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT))) {
request.content(new InputStreamContentProvider(content), contentType);
}
if (logger.isDebugEnabled()) {
logger.debug("About to execute {}", request.getURI());
}
try {
ContentResponse response = request.send();
int statusCode = response.getStatus();
if (statusCode >= HttpStatus.BAD_REQUEST_400) {
String statusLine = statusCode + " " + response.getReason();
logger.debug("Method failed: {}", statusLine);
}
byte[] rawResponse = response.getContent();
String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim()
: "UTF-8";
String responseBody = new String(rawResponse, encoding);
if (!responseBody.isEmpty()) {
logger.trace(responseBody);
}
return responseBody;
} catch (Exception e) {
throw new IOException(e);
} finally {
if (proxy != null) {
// Remove the proxy, that has been added for this request
client.getProxyConfiguration().getProxies().remove(proxy);
}
}
}
/**
* Determines whether the list of <code>nonProxyHosts</code> contains the
* host (which is part of the given <code>urlString</code> or not.
*
* @param urlString
* @param nonProxyHosts
*
* @return <code>false</code> if the host of the given <code>urlString</code> is contained in
* <code>nonProxyHosts</code>-list and <code>true</code> otherwise
*/
private static boolean shouldUseProxy(String urlString, String nonProxyHosts) {
if (StringUtils.isNotBlank(nonProxyHosts)) {
String givenHost = urlString;
try {
URL url = new URL(urlString);
givenHost = url.getHost();
} catch (MalformedURLException e) {
logger.error("the given url {} is malformed", urlString);
}
String[] hosts = nonProxyHosts.split("\\|");
for (String host : hosts) {
if (host.contains("*")) {
// the nonProxyHots-pattern allows wildcards '*' which must
// be masked to be used with regular expressions
String hostRegexp = host.replaceAll("\\.", "\\\\.");
hostRegexp = hostRegexp.replaceAll("\\*", ".*");
if (givenHost.matches(hostRegexp)) {
return false;
}
} else {
if (givenHost.equals(host)) {
return false;
}
}
}
}
return true;
}
/**
* Factory method to create a {@link HttpMethod}-object according to the
* given String <code>httpMethodString</code>
*
* @param httpMethodString the name of the {@link HttpMethod} to create
*
* @throws IllegalArgumentException if <code>httpMethod</code> is none of <code>GET</code>, <code>PUT</code>,
* <code>POST</POST> or <code>DELETE</code>
*/
public static HttpMethod createHttpMethod(String httpMethodString) {
if ("GET".equals(httpMethodString)) {
return HttpMethod.GET;
} else if ("PUT".equals(httpMethodString)) {
return HttpMethod.PUT;
} else if ("POST".equals(httpMethodString)) {
return HttpMethod.POST;
} else if ("DELETE".equals(httpMethodString)) {
return HttpMethod.DELETE;
} else {
throw new IllegalArgumentException("given httpMethod '" + httpMethodString + "' is unknown");
}
}
private static void startHttpClient(HttpClient client) {
if (!client.isStarted()) {
try {
client.start();
} catch (Exception e) {
logger.warn("Cannot start HttpClient!", e);
}
}
}
}