/**
* 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.binding.digitalstrom.internal.lib.serverConnection.impl;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.config.Config;
import org.eclipse.smarthome.binding.digitalstrom.internal.lib.serverConnection.HttpTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HttpTransportImpl} executes an request to the digitalSTROM-Server.
* <p>
* If a {@link Config} is given at the constructor. It sets the SSL-Certificate what is set in
* {@link Config#getCert()}. If there is no SSL-Certificate, but an path to an external SSL-Certificate file what is set
* in {@link Config#getTrustCertPath()} this will be set. If no SSL-Certificate is set in the {@link Config} it will be
* red out from the server and set in {@link Config#setCert(String)}.
* </p>
* <p>
* If no {@link Config} is given the SSL-Certificate will be stored locally.
* </p>
* <p>
* The method {@link #writePEMCertFile(String)} saves the SSL-Certificate in a file at the given path. If all
* SSL-Certificates shout be ignored the flag <i>exeptAllCerts</i> have to be true at the constructor
* </p>
*
* @author Michael Ochel - Initial contribution
* @author Matthias Siegele - Initial contribution
*/
public class HttpTransportImpl implements HttpTransport {
private final static String LINE_SEPERATOR = System.getProperty("line.separator");
private final static String BEGIN_CERT = "-----BEGIN CERTIFICATE-----" + LINE_SEPERATOR;
private final static String END_CERT = LINE_SEPERATOR + "-----END CERTIFICATE-----" + LINE_SEPERATOR;
private static final Logger logger = LoggerFactory.getLogger(HttpTransportImpl.class);
private String uri;
private int connectTimeout;
private int readTimeout;
private Config config = null;
private String cert = null;
private SSLSocketFactory sslSocketFactory = null;
private HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return arg0.equals(arg1.getPeerHost()) || arg0.contains("dss.local.");
}
};
/**
* Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config} and set ignore all
* SSL-Certificates.
*
* @param config
* @param exeptAllCerts (true = all will ignore)
*/
public HttpTransportImpl(Config config, boolean exeptAllCerts) {
this.config = config;
init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), exeptAllCerts);
}
/**
* Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config}.
*
* @param config
*/
public HttpTransportImpl(Config config) {
this.config = config;
init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), false);
}
/**
* Creates a new {@link HttpTransportImpl}.
*
* @param uri
*/
public HttpTransportImpl(String uri) {
init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, false);
}
/**
* Creates a new {@link HttpTransportImpl} and set ignore all SSL-Certificates.
*
* @param uri
* @param exeptAllCerts (true = all will ignore)
*/
public HttpTransportImpl(String uri, boolean exeptAllCerts) {
init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, exeptAllCerts);
}
/**
* Creates a new {@link HttpTransportImpl}.
*
* @param uri
* @param connectTimeout
* @param readTimeout
*/
public HttpTransportImpl(String uri, int connectTimeout, int readTimeout) {
init(uri, connectTimeout, readTimeout, false);
}
public HttpTransportImpl(String uri, int connectTimeout, int readTimeout, boolean exeptAllCerts) {
init(uri, connectTimeout, readTimeout, exeptAllCerts);
}
private void init(String uri, int connectTimeout, int readTimeout, boolean exeptAllCerts) {
this.uri = fixURI(uri);
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
// Check SSL Certificate
if (exeptAllCerts) {
sslSocketFactory = generateSSLContextWhichAcceptAllSSLCertificats();
} else {
if (config != null) {
cert = config.getCert();
logger.debug("generate SSLcontext from config cert");
if (StringUtils.isNotBlank(cert)) {
sslSocketFactory = generateSSLContextFromPEMCertString(cert);
} else {
if (StringUtils.isNotBlank(config.getTrustCertPath())) {
logger.debug("generate SSLcontext from config cert path");
cert = readPEMCertificateStringFromFile(config.getTrustCertPath());
if (StringUtils.isNotBlank(cert)) {
sslSocketFactory = generateSSLContextFromPEMCertString(cert);
}
} else {
logger.debug("generate SSLcontext from server");
cert = getPEMCertificateFromServer(this.uri);
sslSocketFactory = generateSSLContextFromPEMCertString(cert);
if (sslSocketFactory != null) {
config.setCert(cert);
}
}
}
} else {
logger.debug("generate SSLcontext from server");
cert = getPEMCertificateFromServer(this.uri);
sslSocketFactory = generateSSLContextFromPEMCertString(cert);
}
}
}
private String fixURI(String uri) {
if (!uri.startsWith("https://")) {
uri = "https://" + uri;
}
if (uri.split(":").length != 3) {
uri = uri + ":8080";
}
return uri;
}
private String fixRequest(String request) {
return request.replace(" ", "");
}
@Override
public String execute(String request) {
return execute(request, this.connectTimeout, this.readTimeout);
}
@Override
public String execute(String request, int connectTimeout, int readTimeout) {
// NOTE: We will only show exceptions in the debug level, because they will be handled in the checkConnection()
// method and this changes the bridge state. If a command was send it fails than and a sensorJob will be
// execute the next time, by TimeOutExceptions. By other exceptions the checkConnection() method handles it in
// max 1 second.
String response = null;
try {
HttpsURLConnection connection = getConnection(request, connectTimeout, readTimeout);
if (connection != null) {
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
response = IOUtils.toString(connection.getInputStream());
}
connection.disconnect();
return response;
}
} catch (MalformedURLException e) {
} catch (IOException e) {
}
return null;
}
private HttpsURLConnection getConnection(String request, int connectTimeout, int readTimeout) throws IOException {
if (StringUtils.isNotBlank(request)) {
request = fixRequest(request);
URL url = new URL(this.uri + request);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
if (connection != null) {
connection.setConnectTimeout(connectTimeout);
connection.setReadTimeout(readTimeout);
if (sslSocketFactory != null) {
connection.setSSLSocketFactory(sslSocketFactory);
}
if (hostnameVerifier != null) {
connection.setHostnameVerifier(hostnameVerifier);
}
}
return connection;
}
return null;
}
@Override
public int checkConnection(String testRequest) {
try {
HttpsURLConnection connection = getConnection(testRequest, connectTimeout, readTimeout);
if (connection != null) {
connection.connect();
connection.disconnect();
return connection.getResponseCode();
}
} catch (SocketTimeoutException e) {
return -4;
} catch (java.net.ConnectException e) {
return -3;
} catch (MalformedURLException e) {
return -2;
} catch (IOException e) {
if (e instanceof java.net.ConnectException) {
return -3;
}
if (e instanceof java.net.UnknownHostException) {
return -5;
}
logger.error("An IOException occurred by executing jsonRequest: " + testRequest, e);
}
return -1;
}
@Override
public int getSensordataConnectionTimeout() {
return config != null ? config.getSensordataConnectionTimeout() : Config.DEFAULT_SENSORDATA_CONNECTION_TIMEOUT;
}
@Override
public int getSensordataReadTimeout() {
return config != null ? config.getSensordataReadTimeout() : Config.DEFAULT_SENSORDATA_READ_TIMEOUT;
}
private String readPEMCertificateStringFromFile(String path) {
if (StringUtils.isBlank(path)) {
logger.error("Path is empty.");
} else {
File dssCert = new File(path);
if (dssCert.exists()) {
if (path.endsWith(".crt")) {
try {
InputStream certInputStream = new FileInputStream(dssCert);
String cert = IOUtils.toString(certInputStream);
if (cert.startsWith(BEGIN_CERT)) {
return cert;
} else {
logger.error(
"File is not a PEM certificate file. PEM-Certificats starts with: " + BEGIN_CERT);
}
} catch (FileNotFoundException e) {
logger.error("Can't find a certificate file at the path: " + path + "\nPlease check the path!");
} catch (IOException e) {
logger.error("An IOException occurred: ", e);
}
} else {
logger.error("File is not a certificate (.crt) file.");
}
} else {
logger.error("File not found");
}
}
return null;
}
@Override
public String writePEMCertFile(String path) {
path = StringUtils.trimToEmpty(path);
File certFilePath;
if (StringUtils.isNotBlank(path)) {
certFilePath = new File(path);
boolean pathExists = certFilePath.exists();
if (!pathExists) {
pathExists = certFilePath.mkdirs();
}
if (pathExists && !path.endsWith("/")) {
path = path + "/";
}
}
InputStream certInputStream = IOUtils.toInputStream(cert);
X509Certificate trustedCert;
try {
trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(certInputStream);
certFilePath = new File(path + trustedCert.getSubjectDN().getName().split(",")[0].substring(2) + ".crt");
if (!certFilePath.exists()) {
certFilePath.createNewFile();
FileWriter writer = new FileWriter(certFilePath, true);
writer.write(cert);
writer.flush();
writer.close();
return certFilePath.getAbsolutePath();
} else {
logger.error("File allready exists!");
}
} catch (IOException e) {
logger.error("An IOException occurred: ", e);
} catch (CertificateException e1) {
logger.error("A CertificateException occurred: ", e1);
}
return null;
}
private SSLSocketFactory generateSSLContextFromPEMCertString(String pemCert) {
if (StringUtils.isNotBlank(pemCert) && pemCert.startsWith(BEGIN_CERT)) {
try {
InputStream certInputStream = IOUtils.toInputStream(pemCert);
final X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(certInputStream);
final TrustManager[] trustManager = new TrustManager[] { new X509TrustManager() {
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
throws CertificateException {
if (!certs[0].equals(trustedCert)) {
throw new CertificateException();
}
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
throws CertificateException {
if (!certs[0].equals(trustedCert)) {
throw new CertificateException();
}
}
} };
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManager, new java.security.SecureRandom());
return sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException e) {
logger.error("A NoSuchAlgorithmException occurred: ", e);
} catch (KeyManagementException e) {
logger.error("A KeyManagementException occurred: ", e);
} catch (CertificateException e) {
logger.error("A CertificateException occurred: ", e);
}
} else {
logger.error("Cert is empty");
}
return null;
}
private String getPEMCertificateFromServer(String host) {
try {
URL url = new URL(host);
HttpsURLConnection connection = null;
connection = (HttpsURLConnection) url.openConnection();
connection.setHostnameVerifier(hostnameVerifier);
connection.setSSLSocketFactory(generateSSLContextWhichAcceptAllSSLCertificats());
connection.connect();
java.security.cert.Certificate[] cert = connection.getServerCertificates();
connection.disconnect();
byte[] by = ((X509Certificate) cert[0]).getEncoded();
if (by.length != 0) {
return BEGIN_CERT + DatatypeConverter.printBase64Binary(by) + END_CERT;
}
} catch (MalformedURLException e) {
logger.error("A MalformedURLException occurred: ", e);
} catch (IOException e) {
logger.error("An IOException occurred: ", e);
} catch (CertificateEncodingException e) {
logger.error("A CertificateEncodingException occurred: ", e);
}
return null;
}
private SSLSocketFactory generateSSLContextWhichAcceptAllSSLCertificats() {
Security.addProvider(Security.getProvider("SunJCE"));
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
} };
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new SecureRandom());
return sslContext.getSocketFactory();
} catch (KeyManagementException e) {
logger.error("A KeyManagementException occurred", e);
} catch (NoSuchAlgorithmException e) {
logger.error("A NoSuchAlgorithmException occurred", e);
}
return null;
}
}