2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.digitalstrom.internal.lib.serverconnection.impl;
16 import java.io.FileInputStream;
17 import java.io.FileNotFoundException;
18 import java.io.FileWriter;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.net.HttpURLConnection;
22 import java.net.MalformedURLException;
23 import java.net.SocketTimeoutException;
25 import java.security.KeyManagementException;
26 import java.security.NoSuchAlgorithmException;
27 import java.security.SecureRandom;
28 import java.security.Security;
29 import java.security.cert.CertificateEncodingException;
30 import java.security.cert.CertificateException;
31 import java.security.cert.CertificateFactory;
32 import java.security.cert.X509Certificate;
33 import java.util.Base64;
35 import javax.net.ssl.HostnameVerifier;
36 import javax.net.ssl.HttpsURLConnection;
37 import javax.net.ssl.SSLContext;
38 import javax.net.ssl.SSLHandshakeException;
39 import javax.net.ssl.SSLSession;
40 import javax.net.ssl.SSLSocketFactory;
41 import javax.net.ssl.TrustManager;
42 import javax.net.ssl.X509TrustManager;
44 import org.apache.commons.io.IOUtils;
45 import org.apache.commons.lang.StringUtils;
46 import org.openhab.binding.digitalstrom.internal.lib.config.Config;
47 import org.openhab.binding.digitalstrom.internal.lib.manager.ConnectionManager;
48 import org.openhab.binding.digitalstrom.internal.lib.serverconnection.HttpTransport;
49 import org.openhab.binding.digitalstrom.internal.lib.serverconnection.simpledsrequestbuilder.constants.ParameterKeys;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * The {@link HttpTransportImpl} executes an request to the digitalSTROM-Server.
56 * If a {@link Config} is given at the constructor. It sets the SSL-Certificate what is set in
57 * {@link Config#getCert()}. If there is no SSL-Certificate, but an path to an external SSL-Certificate file what is set
58 * in {@link Config#getTrustCertPath()} this will be set. If no SSL-Certificate is set in the {@link Config} it will be
59 * red out from the server and set in {@link Config#setCert(String)}.
62 * If no {@link Config} is given the SSL-Certificate will be stored locally.
65 * The method {@link #writePEMCertFile(String)} saves the SSL-Certificate in a file at the given path. If all
66 * SSL-Certificates shout be ignored the flag <i>exeptAllCerts</i> have to be true at the constructor
69 * If a {@link ConnectionManager} is given at the constructor, the session-token is not needed by requests and the
70 * {@link ConnectionListener}, which is registered at the {@link ConnectionManager}, will be automatically informed
72 * connection state changes through the {@link #execute(String, int, int)} method.
75 * @author Michael Ochel - Initial contribution
76 * @author Matthias Siegele - Initial contribution
78 public class HttpTransportImpl implements HttpTransport {
80 private static final String LINE_SEPERATOR = System.getProperty("line.separator");
81 private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----" + LINE_SEPERATOR;
82 private static final String END_CERT = LINE_SEPERATOR + "-----END CERTIFICATE-----" + LINE_SEPERATOR;
84 private final Logger logger = LoggerFactory.getLogger(HttpTransportImpl.class);
85 private static final short MAY_A_NEW_SESSION_TOKEN_IS_NEEDED = 1;
89 private int connectTimeout;
90 private int readTimeout;
92 private Config config;
94 private ConnectionManager connectionManager;
97 private SSLSocketFactory sslSocketFactory;
98 private final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
101 public boolean verify(String arg0, SSLSession arg1) {
102 return arg0.equals(arg1.getPeerHost()) || arg0.contains("dss.local.");
107 * Creates a new {@link HttpTransportImpl} with registration of the given {@link ConnectionManager} and set ignore
108 * all SSL-Certificates. The {@link Config} will be automatically added from the configurations of the given
109 * {@link ConnectionManager}.
111 * @param connectionManager to check connection, can be null
112 * @param exeptAllCerts (true = all will ignore)
114 public HttpTransportImpl(ConnectionManager connectionManager, boolean exeptAllCerts) {
115 this.connectionManager = connectionManager;
116 this.config = connectionManager.getConfig();
117 init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), exeptAllCerts);
121 * Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config} and set ignore all
124 * @param config to get configurations, must not be null
125 * @param exeptAllCerts (true = all will ignore)
127 public HttpTransportImpl(Config config, boolean exeptAllCerts) {
128 this.config = config;
129 init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), exeptAllCerts);
133 * Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config}.
135 * @param config to get configurations, must not be null
137 public HttpTransportImpl(Config config) {
138 this.config = config;
139 init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), false);
143 * Creates a new {@link HttpTransportImpl}.
145 * @param uri of the server, must not be null
147 public HttpTransportImpl(String uri) {
148 init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, false);
152 * Creates a new {@link HttpTransportImpl} and set ignore all SSL-Certificates.
154 * @param uri of the server, must not be null
155 * @param exeptAllCerts (true = all will ignore)
157 public HttpTransportImpl(String uri, boolean exeptAllCerts) {
158 init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, exeptAllCerts);
162 * Creates a new {@link HttpTransportImpl}.
164 * @param uri of the server, must not be null
165 * @param connectTimeout to set
166 * @param readTimeout to set
168 public HttpTransportImpl(String uri, int connectTimeout, int readTimeout) {
169 init(uri, connectTimeout, readTimeout, false);
173 * Creates a new {@link HttpTransportImpl} and set ignore all SSL-Certificates..
175 * @param uri of the server, must not be null
176 * @param connectTimeout to set
177 * @param readTimeout to set
178 * @param exeptAllCerts (true = all will ignore)
180 public HttpTransportImpl(String uri, int connectTimeout, int readTimeout, boolean exeptAllCerts) {
181 init(uri, connectTimeout, readTimeout, exeptAllCerts);
184 private void init(String uri, int connectTimeout, int readTimeout, boolean exeptAllCerts) {
185 logger.debug("init HttpTransportImpl");
186 this.uri = fixURI(uri);
187 this.connectTimeout = connectTimeout;
188 this.readTimeout = readTimeout;
189 // Check SSL Certificate
191 sslSocketFactory = generateSSLContextWhichAcceptAllSSLCertificats();
193 if (config != null) {
194 cert = config.getCert();
195 logger.debug("generate SSLcontext from config cert");
196 if (StringUtils.isNotBlank(cert)) {
197 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
199 if (StringUtils.isNotBlank(config.getTrustCertPath())) {
200 logger.debug("generate SSLcontext from config cert path");
201 cert = readPEMCertificateStringFromFile(config.getTrustCertPath());
202 if (StringUtils.isNotBlank(cert)) {
203 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
206 logger.debug("generate SSLcontext from server");
207 cert = getPEMCertificateFromServer(this.uri);
208 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
209 if (sslSocketFactory != null) {
210 config.setCert(cert);
215 logger.debug("generate SSLcontext from server");
216 cert = getPEMCertificateFromServer(this.uri);
217 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
222 private String fixURI(String uri) {
223 String fixedURI = uri;
224 if (!fixedURI.startsWith("https://")) {
225 fixedURI = "https://" + fixedURI;
227 if (fixedURI.split(":").length != 3) {
228 fixedURI = fixedURI + ":8080";
233 private String fixRequest(String request) {
234 return request.replace(" ", "");
238 public String execute(String request) {
239 return execute(request, this.connectTimeout, this.readTimeout);
242 private short loginCounter = 0;
245 public String execute(String request, int connectTimeout, int readTimeout) {
246 // NOTE: We will only show exceptions in the debug level, because they will be handled in the checkConnection()
247 // method and this changes the bridge state. If a command was send it fails than and a sensorJob will be
248 // execute the next time, by TimeOutExceptions. By other exceptions the checkConnection() method handles it in
250 String response = null;
251 HttpsURLConnection connection = null;
253 String correctedRequest = checkSessionToken(request);
254 connection = getConnection(correctedRequest, connectTimeout, readTimeout);
255 if (connection != null) {
256 connection.connect();
257 final int responseCode = connection.getResponseCode();
258 if (responseCode != HttpURLConnection.HTTP_FORBIDDEN) {
259 if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
260 response = IOUtils.toString(connection.getErrorStream());
262 response = IOUtils.toString(connection.getInputStream());
264 if (response != null) {
265 if (!response.contains("Authentication failed")) {
266 if (loginCounter > 0) {
267 connectionManager.checkConnection(responseCode);
271 connectionManager.checkConnection(ConnectionManager.AUTHENTIFICATION_PROBLEM);
277 connection.disconnect();
278 if (response == null && connectionManager != null
279 && loginCounter <= MAY_A_NEW_SESSION_TOKEN_IS_NEEDED) {
280 if (responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
281 execute(addSessionToken(correctedRequest, connectionManager.getNewSessionToken()),
282 connectTimeout, readTimeout);
285 connectionManager.checkConnection(responseCode);
292 } catch (SocketTimeoutException e) {
293 informConnectionManager(ConnectionManager.SOCKET_TIMEOUT_EXCEPTION);
294 } catch (java.net.ConnectException e) {
295 informConnectionManager(ConnectionManager.CONNECTION_EXCEPTION);
296 } catch (MalformedURLException e) {
297 informConnectionManager(ConnectionManager.MALFORMED_URL_EXCEPTION);
298 } catch (java.net.UnknownHostException e) {
299 informConnectionManager(ConnectionManager.UNKNOWN_HOST_EXCEPTION);
300 } catch (SSLHandshakeException e) {
301 informConnectionManager(ConnectionManager.SSL_HANDSHAKE_EXCEPTION);
302 } catch (IOException e) {
303 logger.error("An IOException occurred: ", e);
304 informConnectionManager(ConnectionManager.GENERAL_EXCEPTION);
306 if (connection != null) {
307 connection.disconnect();
313 private boolean informConnectionManager(int code) {
314 if (connectionManager != null && loginCounter < MAY_A_NEW_SESSION_TOKEN_IS_NEEDED) {
315 connectionManager.checkConnection(code);
321 private String checkSessionToken(String request) {
322 if (checkNeededSessionToken(request)) {
323 if (connectionManager != null) {
324 String sessionToken = connectionManager.getSessionToken();
325 if (sessionToken == null) {
326 return addSessionToken(request, connectionManager.getNewSessionToken());
328 return addSessionToken(request, sessionToken);
334 private boolean checkNeededSessionToken(String request) {
335 String functionName = StringUtils.substringAfterLast(StringUtils.substringBefore(request, "?"), "/");
336 return !DsAPIImpl.METHODS_MUST_NOT_BE_LOGGED_IN.contains(functionName);
339 private String addSessionToken(String request, String sessionToken) {
340 String correctedRequest = request;
341 if (!correctedRequest.contains(ParameterKeys.TOKEN)) {
342 if (correctedRequest.contains("?")) {
343 correctedRequest = correctedRequest + "&" + ParameterKeys.TOKEN + "=" + sessionToken;
345 correctedRequest = correctedRequest + "?" + ParameterKeys.TOKEN + "=" + sessionToken;
348 correctedRequest = StringUtils.replaceOnce(correctedRequest, StringUtils.substringBefore(
349 StringUtils.substringAfter(correctedRequest, ParameterKeys.TOKEN + "="), "&"), sessionToken);
352 return correctedRequest;
355 private HttpsURLConnection getConnection(String request, int connectTimeout, int readTimeout) throws IOException {
356 String correctedRequest = request;
357 if (StringUtils.isNotBlank(correctedRequest)) {
358 correctedRequest = fixRequest(correctedRequest);
359 URL url = new URL(this.uri + correctedRequest);
360 HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
361 if (connection != null) {
362 connection.setConnectTimeout(connectTimeout);
363 connection.setReadTimeout(readTimeout);
364 if (sslSocketFactory != null) {
365 connection.setSSLSocketFactory(sslSocketFactory);
367 if (hostnameVerifier != null) {
368 connection.setHostnameVerifier(hostnameVerifier);
377 public int checkConnection(String testRequest) {
379 HttpsURLConnection connection = getConnection(testRequest, connectTimeout, readTimeout);
380 if (connection != null) {
381 connection.connect();
382 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
383 if (IOUtils.toString(connection.getInputStream()).contains("Authentication failed")) {
384 return ConnectionManager.AUTHENTIFICATION_PROBLEM;
387 connection.disconnect();
388 return connection.getResponseCode();
390 return ConnectionManager.GENERAL_EXCEPTION;
392 } catch (SocketTimeoutException e) {
393 return ConnectionManager.SOCKET_TIMEOUT_EXCEPTION;
394 } catch (java.net.ConnectException e) {
395 return ConnectionManager.CONNECTION_EXCEPTION;
396 } catch (MalformedURLException e) {
397 return ConnectionManager.MALFORMED_URL_EXCEPTION;
398 } catch (java.net.UnknownHostException e) {
399 return ConnectionManager.UNKNOWN_HOST_EXCEPTION;
400 } catch (IOException e) {
401 return ConnectionManager.GENERAL_EXCEPTION;
406 public int getSensordataConnectionTimeout() {
407 return config != null ? config.getSensordataConnectionTimeout() : Config.DEFAULT_SENSORDATA_CONNECTION_TIMEOUT;
411 public int getSensordataReadTimeout() {
412 return config != null ? config.getSensordataReadTimeout() : Config.DEFAULT_SENSORDATA_READ_TIMEOUT;
415 private String readPEMCertificateStringFromFile(String path) {
416 if (StringUtils.isBlank(path)) {
417 logger.error("Path is empty.");
419 File dssCert = new File(path);
420 if (dssCert.exists()) {
421 if (path.endsWith(".crt")) {
423 InputStream certInputStream = new FileInputStream(dssCert);
424 String cert = IOUtils.toString(certInputStream);
425 if (cert.startsWith(BEGIN_CERT)) {
428 logger.error("File is not a PEM certificate file. PEM-Certificats starts with: {}",
431 } catch (FileNotFoundException e) {
432 logger.error("Can't find a certificate file at the path: {}\nPlease check the path!", path);
433 } catch (IOException e) {
434 logger.error("An IOException occurred: ", e);
437 logger.error("File is not a certificate (.crt) file.");
440 logger.error("File not found");
447 public String writePEMCertFile(String path) {
448 String correctedPath = StringUtils.trimToEmpty(path);
450 if (StringUtils.isNotBlank(correctedPath)) {
451 certFilePath = new File(correctedPath);
452 boolean pathExists = certFilePath.exists();
454 pathExists = certFilePath.mkdirs();
456 if (pathExists && !correctedPath.endsWith("/")) {
457 correctedPath = correctedPath + "/";
460 InputStream certInputStream = IOUtils.toInputStream(cert);
461 X509Certificate trustedCert;
463 trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
464 .generateCertificate(certInputStream);
466 certFilePath = new File(
467 correctedPath + trustedCert.getSubjectDN().getName().split(",")[0].substring(2) + ".crt");
468 if (!certFilePath.exists()) {
469 certFilePath.createNewFile();
470 FileWriter writer = new FileWriter(certFilePath, true);
474 return certFilePath.getAbsolutePath();
476 logger.error("File allready exists!");
478 } catch (IOException e) {
479 logger.error("An IOException occurred: ", e);
480 } catch (CertificateException e1) {
481 logger.error("A CertificateException occurred: ", e1);
486 private SSLSocketFactory generateSSLContextFromPEMCertString(String pemCert) {
487 if (StringUtils.isNotBlank(pemCert) && pemCert.startsWith(BEGIN_CERT)) {
489 InputStream certInputStream = IOUtils.toInputStream(pemCert);
490 final X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
491 .generateCertificate(certInputStream);
493 final TrustManager[] trustManager = new TrustManager[] { new X509TrustManager() {
496 public java.security.cert.X509Certificate[] getAcceptedIssuers() {
501 public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
502 throws CertificateException {
503 if (!certs[0].equals(trustedCert)) {
504 throw new CertificateException();
509 public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
510 throws CertificateException {
511 if (!certs[0].equals(trustedCert)) {
512 throw new CertificateException();
517 SSLContext sslContext = SSLContext.getInstance("SSL");
518 sslContext.init(null, trustManager, new java.security.SecureRandom());
519 return sslContext.getSocketFactory();
520 } catch (NoSuchAlgorithmException e) {
521 logger.error("A NoSuchAlgorithmException occurred: ", e);
522 } catch (KeyManagementException e) {
523 logger.error("A KeyManagementException occurred: ", e);
524 } catch (CertificateException e) {
525 logger.error("A CertificateException occurred: ", e);
528 logger.error("Cert is empty");
533 private String getPEMCertificateFromServer(String host) {
534 HttpsURLConnection connection = null;
536 URL url = new URL(host);
538 connection = (HttpsURLConnection) url.openConnection();
539 connection.setHostnameVerifier(hostnameVerifier);
540 connection.setSSLSocketFactory(generateSSLContextWhichAcceptAllSSLCertificats());
541 connection.connect();
543 java.security.cert.Certificate[] cert = connection.getServerCertificates();
544 connection.disconnect();
546 byte[] by = ((X509Certificate) cert[0]).getEncoded();
547 if (by.length != 0) {
548 return BEGIN_CERT + Base64.getEncoder().encodeToString(by) + END_CERT;
550 } catch (MalformedURLException e) {
551 if (!informConnectionManager(ConnectionManager.MALFORMED_URL_EXCEPTION)) {
552 logger.error("A MalformedURLException occurred: ", e);
554 } catch (IOException e) {
555 short code = ConnectionManager.GENERAL_EXCEPTION;
556 if (e instanceof java.net.ConnectException) {
557 code = ConnectionManager.CONNECTION_EXCEPTION;
558 } else if (e instanceof java.net.UnknownHostException) {
559 code = ConnectionManager.UNKNOWN_HOST_EXCEPTION;
561 if (!informConnectionManager(code) || code == -1) {
562 logger.error("An IOException occurred: ", e);
564 } catch (CertificateEncodingException e) {
565 logger.error("A CertificateEncodingException occurred: ", e);
567 if (connection != null) {
568 connection.disconnect();
574 private SSLSocketFactory generateSSLContextWhichAcceptAllSSLCertificats() {
575 Security.addProvider(Security.getProvider("SunJCE"));
576 TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
579 public java.security.cert.X509Certificate[] getAcceptedIssuers() {
584 public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
588 public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
593 SSLContext sslContext = SSLContext.getInstance("SSL");
595 sslContext.init(null, trustAllCerts, new SecureRandom());
597 return sslContext.getSocketFactory();
598 } catch (KeyManagementException e) {
599 logger.error("A KeyManagementException occurred", e);
600 } catch (NoSuchAlgorithmException e) {
601 logger.error("A NoSuchAlgorithmException occurred", e);