2 * Copyright (c) 2010-2023 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;
15 import java.io.ByteArrayInputStream;
17 import java.io.FileInputStream;
18 import java.io.FileNotFoundException;
19 import java.io.FileWriter;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.net.HttpURLConnection;
23 import java.net.MalformedURLException;
24 import java.net.SocketTimeoutException;
26 import java.nio.charset.StandardCharsets;
27 import java.security.KeyManagementException;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.SecureRandom;
30 import java.security.Security;
31 import java.security.cert.CertificateEncodingException;
32 import java.security.cert.CertificateException;
33 import java.security.cert.CertificateFactory;
34 import java.security.cert.X509Certificate;
35 import java.util.Base64;
37 import javax.net.ssl.HostnameVerifier;
38 import javax.net.ssl.HttpsURLConnection;
39 import javax.net.ssl.SSLContext;
40 import javax.net.ssl.SSLHandshakeException;
41 import javax.net.ssl.SSLSession;
42 import javax.net.ssl.SSLSocketFactory;
43 import javax.net.ssl.TrustManager;
44 import javax.net.ssl.X509TrustManager;
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 a 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 a 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 org.openhab.binding.digitalstrom.internal.lib.listener.ConnectionListener}, which is registered at the
71 * {@link ConnectionManager}, will be automatically informed about
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 (cert != null && !cert.isBlank()) {
197 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
199 String trustCertPath = config.getTrustCertPath();
200 if (trustCertPath != null && !trustCertPath.isBlank()) {
201 logger.debug("generate SSLcontext from config cert path");
202 cert = readPEMCertificateStringFromFile(trustCertPath);
203 if (cert != null && !cert.isBlank()) {
204 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
207 logger.debug("generate SSLcontext from server");
208 cert = getPEMCertificateFromServer(this.uri);
209 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
210 if (sslSocketFactory != null) {
211 config.setCert(cert);
216 logger.debug("generate SSLcontext from server");
217 cert = getPEMCertificateFromServer(this.uri);
218 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
223 private String fixURI(String uri) {
224 String fixedURI = uri;
225 if (!fixedURI.startsWith("https://")) {
226 fixedURI = "https://" + fixedURI;
228 if (fixedURI.split(":").length != 3) {
229 fixedURI = fixedURI + ":8080";
234 private String fixRequest(String request) {
235 return request.replace(" ", "");
239 public String execute(String request) {
240 return execute(request, this.connectTimeout, this.readTimeout);
243 private short loginCounter = 0;
246 public String execute(String request, int connectTimeout, int readTimeout) {
247 // NOTE: We will only show exceptions in the debug level, because they will be handled in the checkConnection()
248 // method and this changes the bridge state. If a command was send it fails than and a sensorJob will be
249 // execute the next time, by TimeOutExceptions. By other exceptions the checkConnection() method handles it in
251 String response = null;
252 HttpsURLConnection connection = null;
254 String correctedRequest = checkSessionToken(request);
255 connection = getConnection(correctedRequest, connectTimeout, readTimeout);
256 if (connection != null) {
257 connection.connect();
258 final int responseCode = connection.getResponseCode();
259 if (responseCode != HttpURLConnection.HTTP_FORBIDDEN) {
260 if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
261 response = new String(connection.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
263 response = new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
265 if (response != null) {
266 if (!response.contains("Authentication failed")) {
267 if (loginCounter > 0) {
268 connectionManager.checkConnection(responseCode);
272 connectionManager.checkConnection(ConnectionManager.AUTHENTIFICATION_PROBLEM);
278 connection.disconnect();
279 if (response == null && connectionManager != null
280 && loginCounter <= MAY_A_NEW_SESSION_TOKEN_IS_NEEDED) {
281 if (responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
282 execute(addSessionToken(correctedRequest, connectionManager.getNewSessionToken()),
283 connectTimeout, readTimeout);
286 connectionManager.checkConnection(responseCode);
293 } catch (SocketTimeoutException e) {
294 informConnectionManager(ConnectionManager.SOCKET_TIMEOUT_EXCEPTION);
295 } catch (java.net.ConnectException e) {
296 informConnectionManager(ConnectionManager.CONNECTION_EXCEPTION);
297 } catch (MalformedURLException e) {
298 informConnectionManager(ConnectionManager.MALFORMED_URL_EXCEPTION);
299 } catch (java.net.UnknownHostException e) {
300 informConnectionManager(ConnectionManager.UNKNOWN_HOST_EXCEPTION);
301 } catch (SSLHandshakeException e) {
302 informConnectionManager(ConnectionManager.SSL_HANDSHAKE_EXCEPTION);
303 } catch (IOException e) {
304 logger.error("An IOException occurred: ", e);
305 informConnectionManager(ConnectionManager.GENERAL_EXCEPTION);
307 if (connection != null) {
308 connection.disconnect();
314 private boolean informConnectionManager(int code) {
315 if (connectionManager != null && loginCounter < MAY_A_NEW_SESSION_TOKEN_IS_NEEDED) {
316 connectionManager.checkConnection(code);
322 private String checkSessionToken(String request) {
323 if (checkNeededSessionToken(request)) {
324 if (connectionManager != null) {
325 String sessionToken = connectionManager.getSessionToken();
326 if (sessionToken == null) {
327 return addSessionToken(request, connectionManager.getNewSessionToken());
329 return addSessionToken(request, sessionToken);
335 private boolean checkNeededSessionToken(String request) {
336 String requestFirstPart = request;
337 int indexOfSeparator = request.indexOf("?");
338 if (indexOfSeparator >= 0) {
339 requestFirstPart = request.substring(0, request.indexOf("?"));
341 String functionName = requestFirstPart.substring(requestFirstPart.lastIndexOf("/") + 1);
342 return !DsAPIImpl.METHODS_MUST_NOT_BE_LOGGED_IN.contains(functionName);
345 private String addSessionToken(String request, String sessionToken) {
346 String correctedRequest = request;
347 if (!correctedRequest.contains(ParameterKeys.TOKEN)) {
348 if (correctedRequest.contains("?")) {
349 correctedRequest = correctedRequest + "&" + ParameterKeys.TOKEN + "=" + sessionToken;
351 correctedRequest = correctedRequest + "?" + ParameterKeys.TOKEN + "=" + sessionToken;
354 String strippedRequest = correctedRequest
355 .substring(correctedRequest.indexOf(ParameterKeys.TOKEN + "=") + ParameterKeys.TOKEN.length() + 1);
356 int indexOfSeparator = strippedRequest.indexOf("&");
357 if (indexOfSeparator >= 0) {
358 strippedRequest = strippedRequest.substring(0, indexOfSeparator);
360 correctedRequest = correctedRequest.replaceFirst(strippedRequest, sessionToken);
362 return correctedRequest;
365 private HttpsURLConnection getConnection(String request, int connectTimeout, int readTimeout) throws IOException {
366 String correctedRequest = request;
367 if (correctedRequest != null && !correctedRequest.isBlank()) {
368 correctedRequest = fixRequest(correctedRequest);
369 URL url = new URL(this.uri + correctedRequest);
370 HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
371 if (connection != null) {
372 connection.setConnectTimeout(connectTimeout);
373 connection.setReadTimeout(readTimeout);
374 if (sslSocketFactory != null) {
375 connection.setSSLSocketFactory(sslSocketFactory);
377 if (hostnameVerifier != null) {
378 connection.setHostnameVerifier(hostnameVerifier);
387 public int checkConnection(String testRequest) {
389 HttpsURLConnection connection = getConnection(testRequest, connectTimeout, readTimeout);
390 if (connection != null) {
391 connection.connect();
392 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
393 if (new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8)
394 .contains("Authentication failed")) {
395 return ConnectionManager.AUTHENTIFICATION_PROBLEM;
398 connection.disconnect();
399 return connection.getResponseCode();
401 return ConnectionManager.GENERAL_EXCEPTION;
403 } catch (SocketTimeoutException e) {
404 return ConnectionManager.SOCKET_TIMEOUT_EXCEPTION;
405 } catch (java.net.ConnectException e) {
406 return ConnectionManager.CONNECTION_EXCEPTION;
407 } catch (MalformedURLException e) {
408 return ConnectionManager.MALFORMED_URL_EXCEPTION;
409 } catch (java.net.UnknownHostException e) {
410 return ConnectionManager.UNKNOWN_HOST_EXCEPTION;
411 } catch (IOException e) {
412 return ConnectionManager.GENERAL_EXCEPTION;
417 public int getSensordataConnectionTimeout() {
418 return config != null ? config.getSensordataConnectionTimeout() : Config.DEFAULT_SENSORDATA_CONNECTION_TIMEOUT;
422 public int getSensordataReadTimeout() {
423 return config != null ? config.getSensordataReadTimeout() : Config.DEFAULT_SENSORDATA_READ_TIMEOUT;
426 private String readPEMCertificateStringFromFile(String path) {
427 if (path == null || path.isBlank()) {
428 logger.error("Path is empty.");
430 File dssCert = new File(path);
431 if (dssCert.exists()) {
432 if (path.endsWith(".crt")) {
433 try (InputStream certInputStream = new FileInputStream(dssCert)) {
434 String cert = new String(certInputStream.readAllBytes(), StandardCharsets.UTF_8);
435 if (cert.startsWith(BEGIN_CERT)) {
438 logger.error("File is not a PEM certificate file. PEM-Certificates starts with: {}",
441 } catch (FileNotFoundException e) {
442 logger.error("Can't find a certificate file at the path: {}\nPlease check the path!", path);
443 } catch (IOException e) {
444 logger.error("An IOException occurred: ", e);
447 logger.error("File is not a certificate (.crt) file.");
450 logger.error("File not found");
457 public String writePEMCertFile(String path) {
458 String correctedPath = path == null ? "" : path.trim();
460 if (!correctedPath.isBlank()) {
461 certFilePath = new File(correctedPath);
462 boolean pathExists = certFilePath.exists();
464 pathExists = certFilePath.mkdirs();
466 if (pathExists && !correctedPath.endsWith("/")) {
467 correctedPath = correctedPath + "/";
470 InputStream certInputStream = new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8));
471 X509Certificate trustedCert;
473 trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
474 .generateCertificate(certInputStream);
476 certFilePath = new File(
477 correctedPath + trustedCert.getSubjectDN().getName().split(",")[0].substring(2) + ".crt");
478 if (!certFilePath.exists()) {
479 certFilePath.createNewFile();
480 FileWriter writer = new FileWriter(certFilePath, true);
484 return certFilePath.getAbsolutePath();
486 logger.error("File allready exists!");
488 } catch (IOException e) {
489 logger.error("An IOException occurred: ", e);
490 } catch (CertificateException e1) {
491 logger.error("A CertificateException occurred: ", e1);
496 private SSLSocketFactory generateSSLContextFromPEMCertString(String pemCert) {
497 if (pemCert != null && !pemCert.isBlank() && pemCert.startsWith(BEGIN_CERT)) {
499 InputStream certInputStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
500 final X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
501 .generateCertificate(certInputStream);
503 final TrustManager[] trustManager = new TrustManager[] { new X509TrustManager() {
506 public java.security.cert.X509Certificate[] getAcceptedIssuers() {
511 public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
512 throws CertificateException {
513 if (!certs[0].equals(trustedCert)) {
514 throw new CertificateException();
519 public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
520 throws CertificateException {
521 if (!certs[0].equals(trustedCert)) {
522 throw new CertificateException();
527 SSLContext sslContext = SSLContext.getInstance("SSL");
528 sslContext.init(null, trustManager, new java.security.SecureRandom());
529 return sslContext.getSocketFactory();
530 } catch (NoSuchAlgorithmException e) {
531 logger.error("A NoSuchAlgorithmException occurred: ", e);
532 } catch (KeyManagementException e) {
533 logger.error("A KeyManagementException occurred: ", e);
534 } catch (CertificateException e) {
535 logger.error("A CertificateException occurred: ", e);
538 logger.error("Cert is empty");
543 private String getPEMCertificateFromServer(String host) {
544 HttpsURLConnection connection = null;
546 URL url = new URL(host);
548 connection = (HttpsURLConnection) url.openConnection();
549 connection.setHostnameVerifier(hostnameVerifier);
550 connection.setSSLSocketFactory(generateSSLContextWhichAcceptAllSSLCertificats());
551 connection.connect();
553 java.security.cert.Certificate[] cert = connection.getServerCertificates();
554 connection.disconnect();
556 byte[] by = ((X509Certificate) cert[0]).getEncoded();
557 if (by.length != 0) {
558 return BEGIN_CERT + Base64.getEncoder().encodeToString(by) + END_CERT;
560 } catch (MalformedURLException e) {
561 if (!informConnectionManager(ConnectionManager.MALFORMED_URL_EXCEPTION)) {
562 logger.error("A MalformedURLException occurred: ", e);
564 } catch (IOException e) {
565 short code = ConnectionManager.GENERAL_EXCEPTION;
566 if (e instanceof java.net.ConnectException) {
567 code = ConnectionManager.CONNECTION_EXCEPTION;
568 } else if (e instanceof java.net.UnknownHostException) {
569 code = ConnectionManager.UNKNOWN_HOST_EXCEPTION;
571 if (!informConnectionManager(code) || code == -1) {
572 logger.error("An IOException occurred: ", e);
574 } catch (CertificateEncodingException e) {
575 logger.error("A CertificateEncodingException occurred: ", e);
577 if (connection != null) {
578 connection.disconnect();
584 private SSLSocketFactory generateSSLContextWhichAcceptAllSSLCertificats() {
585 Security.addProvider(Security.getProvider("SunJCE"));
586 TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
589 public java.security.cert.X509Certificate[] getAcceptedIssuers() {
594 public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
598 public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
603 SSLContext sslContext = SSLContext.getInstance("SSL");
605 sslContext.init(null, trustAllCerts, new SecureRandom());
607 return sslContext.getSocketFactory();
608 } catch (KeyManagementException e) {
609 logger.error("A KeyManagementException occurred", e);
610 } catch (NoSuchAlgorithmException e) {
611 logger.error("A NoSuchAlgorithmException occurred", e);