]> git.basschouten.com Git - openhab-addons.git/blob
ff8adab9bef9dd07e3268a373a3e8c2ae6d616c1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.digitalstrom.internal.lib.serverconnection.impl;
14
15 import java.io.ByteArrayInputStream;
16 import java.io.File;
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;
25 import java.net.URL;
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;
36
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;
45
46 import org.apache.commons.lang3.StringUtils;
47 import org.openhab.binding.digitalstrom.internal.lib.config.Config;
48 import org.openhab.binding.digitalstrom.internal.lib.manager.ConnectionManager;
49 import org.openhab.binding.digitalstrom.internal.lib.serverconnection.HttpTransport;
50 import org.openhab.binding.digitalstrom.internal.lib.serverconnection.simpledsrequestbuilder.constants.ParameterKeys;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 /**
55  * The {@link HttpTransportImpl} executes a request to the digitalSTROM-Server.
56  * <p>
57  * If a {@link Config} is given at the constructor. It sets the SSL-Certificate what is set in
58  * {@link Config#getCert()}. If there is no SSL-Certificate, but a path to an external SSL-Certificate file what is set
59  * in {@link Config#getTrustCertPath()} this will be set. If no SSL-Certificate is set in the {@link Config} it will be
60  * red out from the server and set in {@link Config#setCert(String)}.
61  *
62  * <p>
63  * If no {@link Config} is given the SSL-Certificate will be stored locally.
64  *
65  * <p>
66  * The method {@link #writePEMCertFile(String)} saves the SSL-Certificate in a file at the given path. If all
67  * SSL-Certificates shout be ignored the flag <i>exeptAllCerts</i> have to be true at the constructor
68  * </p>
69  * <p>
70  * If a {@link ConnectionManager} is given at the constructor, the session-token is not needed by requests and the
71  * {@link ConnectionListener}, which is registered at the {@link ConnectionManager}, will be automatically informed
72  * about
73  * connection state changes through the {@link #execute(String, int, int)} method.
74  * </p>
75  *
76  * @author Michael Ochel - Initial contribution
77  * @author Matthias Siegele - Initial contribution
78  */
79 public class HttpTransportImpl implements HttpTransport {
80
81     private static final String LINE_SEPERATOR = System.getProperty("line.separator");
82     private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----" + LINE_SEPERATOR;
83     private static final String END_CERT = LINE_SEPERATOR + "-----END CERTIFICATE-----" + LINE_SEPERATOR;
84
85     private final Logger logger = LoggerFactory.getLogger(HttpTransportImpl.class);
86     private static final short MAY_A_NEW_SESSION_TOKEN_IS_NEEDED = 1;
87
88     private String uri;
89
90     private int connectTimeout;
91     private int readTimeout;
92
93     private Config config;
94
95     private ConnectionManager connectionManager;
96
97     private String cert;
98     private SSLSocketFactory sslSocketFactory;
99     private final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
100
101         @Override
102         public boolean verify(String arg0, SSLSession arg1) {
103             return arg0.equals(arg1.getPeerHost()) || arg0.contains("dss.local.");
104         }
105     };
106
107     /**
108      * Creates a new {@link HttpTransportImpl} with registration of the given {@link ConnectionManager} and set ignore
109      * all SSL-Certificates. The {@link Config} will be automatically added from the configurations of the given
110      * {@link ConnectionManager}.
111      *
112      * @param connectionManager to check connection, can be null
113      * @param exeptAllCerts (true = all will ignore)
114      */
115     public HttpTransportImpl(ConnectionManager connectionManager, boolean exeptAllCerts) {
116         this.connectionManager = connectionManager;
117         this.config = connectionManager.getConfig();
118         init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), exeptAllCerts);
119     }
120
121     /**
122      * Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config} and set ignore all
123      * SSL-Certificates.
124      *
125      * @param config to get configurations, must not be null
126      * @param exeptAllCerts (true = all will ignore)
127      */
128     public HttpTransportImpl(Config config, boolean exeptAllCerts) {
129         this.config = config;
130         init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), exeptAllCerts);
131     }
132
133     /**
134      * Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config}.
135      *
136      * @param config to get configurations, must not be null
137      */
138     public HttpTransportImpl(Config config) {
139         this.config = config;
140         init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), false);
141     }
142
143     /**
144      * Creates a new {@link HttpTransportImpl}.
145      *
146      * @param uri of the server, must not be null
147      */
148     public HttpTransportImpl(String uri) {
149         init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, false);
150     }
151
152     /**
153      * Creates a new {@link HttpTransportImpl} and set ignore all SSL-Certificates.
154      *
155      * @param uri of the server, must not be null
156      * @param exeptAllCerts (true = all will ignore)
157      */
158     public HttpTransportImpl(String uri, boolean exeptAllCerts) {
159         init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, exeptAllCerts);
160     }
161
162     /**
163      * Creates a new {@link HttpTransportImpl}.
164      *
165      * @param uri of the server, must not be null
166      * @param connectTimeout to set
167      * @param readTimeout to set
168      */
169     public HttpTransportImpl(String uri, int connectTimeout, int readTimeout) {
170         init(uri, connectTimeout, readTimeout, false);
171     }
172
173     /**
174      * Creates a new {@link HttpTransportImpl} and set ignore all SSL-Certificates..
175      *
176      * @param uri of the server, must not be null
177      * @param connectTimeout to set
178      * @param readTimeout to set
179      * @param exeptAllCerts (true = all will ignore)
180      */
181     public HttpTransportImpl(String uri, int connectTimeout, int readTimeout, boolean exeptAllCerts) {
182         init(uri, connectTimeout, readTimeout, exeptAllCerts);
183     }
184
185     private void init(String uri, int connectTimeout, int readTimeout, boolean exeptAllCerts) {
186         logger.debug("init HttpTransportImpl");
187         this.uri = fixURI(uri);
188         this.connectTimeout = connectTimeout;
189         this.readTimeout = readTimeout;
190         // Check SSL Certificate
191         if (exeptAllCerts) {
192             sslSocketFactory = generateSSLContextWhichAcceptAllSSLCertificats();
193         } else {
194             if (config != null) {
195                 cert = config.getCert();
196                 logger.debug("generate SSLcontext from config cert");
197                 if (cert != null && !cert.isBlank()) {
198                     sslSocketFactory = generateSSLContextFromPEMCertString(cert);
199                 } else {
200                     String trustCertPath = config.getTrustCertPath();
201                     if (trustCertPath != null && !trustCertPath.isBlank()) {
202                         logger.debug("generate SSLcontext from config cert path");
203                         cert = readPEMCertificateStringFromFile(trustCertPath);
204                         if (cert != null && !cert.isBlank()) {
205                             sslSocketFactory = generateSSLContextFromPEMCertString(cert);
206                         }
207                     } else {
208                         logger.debug("generate SSLcontext from server");
209                         cert = getPEMCertificateFromServer(this.uri);
210                         sslSocketFactory = generateSSLContextFromPEMCertString(cert);
211                         if (sslSocketFactory != null) {
212                             config.setCert(cert);
213                         }
214                     }
215                 }
216             } else {
217                 logger.debug("generate SSLcontext from server");
218                 cert = getPEMCertificateFromServer(this.uri);
219                 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
220             }
221         }
222     }
223
224     private String fixURI(String uri) {
225         String fixedURI = uri;
226         if (!fixedURI.startsWith("https://")) {
227             fixedURI = "https://" + fixedURI;
228         }
229         if (fixedURI.split(":").length != 3) {
230             fixedURI = fixedURI + ":8080";
231         }
232         return fixedURI;
233     }
234
235     private String fixRequest(String request) {
236         return request.replace(" ", "");
237     }
238
239     @Override
240     public String execute(String request) {
241         return execute(request, this.connectTimeout, this.readTimeout);
242     }
243
244     private short loginCounter = 0;
245
246     @Override
247     public String execute(String request, int connectTimeout, int readTimeout) {
248         // NOTE: We will only show exceptions in the debug level, because they will be handled in the checkConnection()
249         // method and this changes the bridge state. If a command was send it fails than and a sensorJob will be
250         // execute the next time, by TimeOutExceptions. By other exceptions the checkConnection() method handles it in
251         // max 1 second.
252         String response = null;
253         HttpsURLConnection connection = null;
254         try {
255             String correctedRequest = checkSessionToken(request);
256             connection = getConnection(correctedRequest, connectTimeout, readTimeout);
257             if (connection != null) {
258                 connection.connect();
259                 final int responseCode = connection.getResponseCode();
260                 if (responseCode != HttpURLConnection.HTTP_FORBIDDEN) {
261                     if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) {
262                         response = new String(connection.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
263                     } else {
264                         response = new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
265                     }
266                     if (response != null) {
267                         if (!response.contains("Authentication failed")) {
268                             if (loginCounter > 0) {
269                                 connectionManager.checkConnection(responseCode);
270                             }
271                             loginCounter = 0;
272                         } else {
273                             connectionManager.checkConnection(ConnectionManager.AUTHENTIFICATION_PROBLEM);
274                             loginCounter++;
275                         }
276                     }
277
278                 }
279                 connection.disconnect();
280                 if (response == null && connectionManager != null
281                         && loginCounter <= MAY_A_NEW_SESSION_TOKEN_IS_NEEDED) {
282                     if (responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
283                         execute(addSessionToken(correctedRequest, connectionManager.getNewSessionToken()),
284                                 connectTimeout, readTimeout);
285                         loginCounter++;
286                     } else {
287                         connectionManager.checkConnection(responseCode);
288                         loginCounter++;
289                         return null;
290                     }
291                 }
292                 return response;
293             }
294         } catch (SocketTimeoutException e) {
295             informConnectionManager(ConnectionManager.SOCKET_TIMEOUT_EXCEPTION);
296         } catch (java.net.ConnectException e) {
297             informConnectionManager(ConnectionManager.CONNECTION_EXCEPTION);
298         } catch (MalformedURLException e) {
299             informConnectionManager(ConnectionManager.MALFORMED_URL_EXCEPTION);
300         } catch (java.net.UnknownHostException e) {
301             informConnectionManager(ConnectionManager.UNKNOWN_HOST_EXCEPTION);
302         } catch (SSLHandshakeException e) {
303             informConnectionManager(ConnectionManager.SSL_HANDSHAKE_EXCEPTION);
304         } catch (IOException e) {
305             logger.error("An IOException occurred: ", e);
306             informConnectionManager(ConnectionManager.GENERAL_EXCEPTION);
307         } finally {
308             if (connection != null) {
309                 connection.disconnect();
310             }
311         }
312         return null;
313     }
314
315     private boolean informConnectionManager(int code) {
316         if (connectionManager != null && loginCounter < MAY_A_NEW_SESSION_TOKEN_IS_NEEDED) {
317             connectionManager.checkConnection(code);
318             return true;
319         }
320         return false;
321     }
322
323     private String checkSessionToken(String request) {
324         if (checkNeededSessionToken(request)) {
325             if (connectionManager != null) {
326                 String sessionToken = connectionManager.getSessionToken();
327                 if (sessionToken == null) {
328                     return addSessionToken(request, connectionManager.getNewSessionToken());
329                 }
330                 return addSessionToken(request, sessionToken);
331             }
332         }
333         return request;
334     }
335
336     private boolean checkNeededSessionToken(String request) {
337         String functionName = StringUtils.substringAfterLast(StringUtils.substringBefore(request, "?"), "/");
338         return !DsAPIImpl.METHODS_MUST_NOT_BE_LOGGED_IN.contains(functionName);
339     }
340
341     private String addSessionToken(String request, String sessionToken) {
342         String correctedRequest = request;
343         if (!correctedRequest.contains(ParameterKeys.TOKEN)) {
344             if (correctedRequest.contains("?")) {
345                 correctedRequest = correctedRequest + "&" + ParameterKeys.TOKEN + "=" + sessionToken;
346             } else {
347                 correctedRequest = correctedRequest + "?" + ParameterKeys.TOKEN + "=" + sessionToken;
348             }
349         } else {
350             correctedRequest = StringUtils.replaceOnce(correctedRequest, StringUtils.substringBefore(
351                     StringUtils.substringAfter(correctedRequest, ParameterKeys.TOKEN + "="), "&"), sessionToken);
352         }
353         return correctedRequest;
354     }
355
356     private HttpsURLConnection getConnection(String request, int connectTimeout, int readTimeout) throws IOException {
357         String correctedRequest = request;
358         if (correctedRequest != null && !correctedRequest.isBlank()) {
359             correctedRequest = fixRequest(correctedRequest);
360             URL url = new URL(this.uri + correctedRequest);
361             HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
362             if (connection != null) {
363                 connection.setConnectTimeout(connectTimeout);
364                 connection.setReadTimeout(readTimeout);
365                 if (sslSocketFactory != null) {
366                     connection.setSSLSocketFactory(sslSocketFactory);
367                 }
368                 if (hostnameVerifier != null) {
369                     connection.setHostnameVerifier(hostnameVerifier);
370                 }
371             }
372             return connection;
373         }
374         return null;
375     }
376
377     @Override
378     public int checkConnection(String testRequest) {
379         try {
380             HttpsURLConnection connection = getConnection(testRequest, connectTimeout, readTimeout);
381             if (connection != null) {
382                 connection.connect();
383                 if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
384                     if (new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8)
385                             .contains("Authentication failed")) {
386                         return ConnectionManager.AUTHENTIFICATION_PROBLEM;
387                     }
388                 }
389                 connection.disconnect();
390                 return connection.getResponseCode();
391             } else {
392                 return ConnectionManager.GENERAL_EXCEPTION;
393             }
394         } catch (SocketTimeoutException e) {
395             return ConnectionManager.SOCKET_TIMEOUT_EXCEPTION;
396         } catch (java.net.ConnectException e) {
397             return ConnectionManager.CONNECTION_EXCEPTION;
398         } catch (MalformedURLException e) {
399             return ConnectionManager.MALFORMED_URL_EXCEPTION;
400         } catch (java.net.UnknownHostException e) {
401             return ConnectionManager.UNKNOWN_HOST_EXCEPTION;
402         } catch (IOException e) {
403             return ConnectionManager.GENERAL_EXCEPTION;
404         }
405     }
406
407     @Override
408     public int getSensordataConnectionTimeout() {
409         return config != null ? config.getSensordataConnectionTimeout() : Config.DEFAULT_SENSORDATA_CONNECTION_TIMEOUT;
410     }
411
412     @Override
413     public int getSensordataReadTimeout() {
414         return config != null ? config.getSensordataReadTimeout() : Config.DEFAULT_SENSORDATA_READ_TIMEOUT;
415     }
416
417     private String readPEMCertificateStringFromFile(String path) {
418         if (path == null || path.isBlank()) {
419             logger.error("Path is empty.");
420         } else {
421             File dssCert = new File(path);
422             if (dssCert.exists()) {
423                 if (path.endsWith(".crt")) {
424                     try (InputStream certInputStream = new FileInputStream(dssCert)) {
425                         String cert = new String(certInputStream.readAllBytes(), StandardCharsets.UTF_8);
426                         if (cert.startsWith(BEGIN_CERT)) {
427                             return cert;
428                         } else {
429                             logger.error("File is not a PEM certificate file. PEM-Certificates starts with: {}",
430                                     BEGIN_CERT);
431                         }
432                     } catch (FileNotFoundException e) {
433                         logger.error("Can't find a certificate file at the path: {}\nPlease check the path!", path);
434                     } catch (IOException e) {
435                         logger.error("An IOException occurred: ", e);
436                     }
437                 } else {
438                     logger.error("File is not a certificate (.crt) file.");
439                 }
440             } else {
441                 logger.error("File not found");
442             }
443         }
444         return null;
445     }
446
447     @Override
448     public String writePEMCertFile(String path) {
449         String correctedPath = path == null ? "" : path.trim();
450         File certFilePath;
451         if (!correctedPath.isBlank()) {
452             certFilePath = new File(correctedPath);
453             boolean pathExists = certFilePath.exists();
454             if (!pathExists) {
455                 pathExists = certFilePath.mkdirs();
456             }
457             if (pathExists && !correctedPath.endsWith("/")) {
458                 correctedPath = correctedPath + "/";
459             }
460         }
461         InputStream certInputStream = new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8));
462         X509Certificate trustedCert;
463         try {
464             trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
465                     .generateCertificate(certInputStream);
466
467             certFilePath = new File(
468                     correctedPath + trustedCert.getSubjectDN().getName().split(",")[0].substring(2) + ".crt");
469             if (!certFilePath.exists()) {
470                 certFilePath.createNewFile();
471                 FileWriter writer = new FileWriter(certFilePath, true);
472                 writer.write(cert);
473                 writer.flush();
474                 writer.close();
475                 return certFilePath.getAbsolutePath();
476             } else {
477                 logger.error("File allready exists!");
478             }
479         } catch (IOException e) {
480             logger.error("An IOException occurred: ", e);
481         } catch (CertificateException e1) {
482             logger.error("A CertificateException occurred: ", e1);
483         }
484         return null;
485     }
486
487     private SSLSocketFactory generateSSLContextFromPEMCertString(String pemCert) {
488         if (pemCert != null && !pemCert.isBlank() && pemCert.startsWith(BEGIN_CERT)) {
489             try {
490                 InputStream certInputStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
491                 final X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
492                         .generateCertificate(certInputStream);
493
494                 final TrustManager[] trustManager = new TrustManager[] { new X509TrustManager() {
495
496                     @Override
497                     public java.security.cert.X509Certificate[] getAcceptedIssuers() {
498                         return null;
499                     }
500
501                     @Override
502                     public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
503                             throws CertificateException {
504                         if (!certs[0].equals(trustedCert)) {
505                             throw new CertificateException();
506                         }
507                     }
508
509                     @Override
510                     public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
511                             throws CertificateException {
512                         if (!certs[0].equals(trustedCert)) {
513                             throw new CertificateException();
514                         }
515                     }
516                 } };
517
518                 SSLContext sslContext = SSLContext.getInstance("SSL");
519                 sslContext.init(null, trustManager, new java.security.SecureRandom());
520                 return sslContext.getSocketFactory();
521             } catch (NoSuchAlgorithmException e) {
522                 logger.error("A NoSuchAlgorithmException occurred: ", e);
523             } catch (KeyManagementException e) {
524                 logger.error("A KeyManagementException occurred: ", e);
525             } catch (CertificateException e) {
526                 logger.error("A CertificateException occurred: ", e);
527             }
528         } else {
529             logger.error("Cert is empty");
530         }
531         return null;
532     }
533
534     private String getPEMCertificateFromServer(String host) {
535         HttpsURLConnection connection = null;
536         try {
537             URL url = new URL(host);
538
539             connection = (HttpsURLConnection) url.openConnection();
540             connection.setHostnameVerifier(hostnameVerifier);
541             connection.setSSLSocketFactory(generateSSLContextWhichAcceptAllSSLCertificats());
542             connection.connect();
543
544             java.security.cert.Certificate[] cert = connection.getServerCertificates();
545             connection.disconnect();
546
547             byte[] by = ((X509Certificate) cert[0]).getEncoded();
548             if (by.length != 0) {
549                 return BEGIN_CERT + Base64.getEncoder().encodeToString(by) + END_CERT;
550             }
551         } catch (MalformedURLException e) {
552             if (!informConnectionManager(ConnectionManager.MALFORMED_URL_EXCEPTION)) {
553                 logger.error("A MalformedURLException occurred: ", e);
554             }
555         } catch (IOException e) {
556             short code = ConnectionManager.GENERAL_EXCEPTION;
557             if (e instanceof java.net.ConnectException) {
558                 code = ConnectionManager.CONNECTION_EXCEPTION;
559             } else if (e instanceof java.net.UnknownHostException) {
560                 code = ConnectionManager.UNKNOWN_HOST_EXCEPTION;
561             }
562             if (!informConnectionManager(code) || code == -1) {
563                 logger.error("An IOException occurred: ", e);
564             }
565         } catch (CertificateEncodingException e) {
566             logger.error("A CertificateEncodingException occurred: ", e);
567         } finally {
568             if (connection != null) {
569                 connection.disconnect();
570             }
571         }
572         return null;
573     }
574
575     private SSLSocketFactory generateSSLContextWhichAcceptAllSSLCertificats() {
576         Security.addProvider(Security.getProvider("SunJCE"));
577         TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
578
579             @Override
580             public java.security.cert.X509Certificate[] getAcceptedIssuers() {
581                 return null;
582             }
583
584             @Override
585             public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
586             }
587
588             @Override
589             public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
590             }
591         } };
592
593         try {
594             SSLContext sslContext = SSLContext.getInstance("SSL");
595
596             sslContext.init(null, trustAllCerts, new SecureRandom());
597
598             return sslContext.getSocketFactory();
599         } catch (KeyManagementException e) {
600             logger.error("A KeyManagementException occurred", e);
601         } catch (NoSuchAlgorithmException e) {
602             logger.error("A NoSuchAlgorithmException occurred", e);
603         }
604         return null;
605     }
606 }