]> git.basschouten.com Git - openhab-addons.git/blob
77cea9a99a831ed32a4a89b632aa2fdf5c0a976d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.File;
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;
24 import java.net.URL;
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;
34
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;
43
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;
52
53 /**
54  * The {@link HttpTransportImpl} executes an request to the digitalSTROM-Server.
55  * <p>
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)}.
60  *
61  * <p>
62  * If no {@link Config} is given the SSL-Certificate will be stored locally.
63  *
64  * <p>
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
67  * </p>
68  * <p>
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
71  * about
72  * connection state changes through the {@link #execute(String, int, int)} method.
73  * </p>
74  *
75  * @author Michael Ochel - Initial contribution
76  * @author Matthias Siegele - Initial contribution
77  */
78 public class HttpTransportImpl implements HttpTransport {
79
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;
83
84     private final Logger logger = LoggerFactory.getLogger(HttpTransportImpl.class);
85     private static final short MAY_A_NEW_SESSION_TOKEN_IS_NEEDED = 1;
86
87     private String uri;
88
89     private int connectTimeout;
90     private int readTimeout;
91
92     private Config config;
93
94     private ConnectionManager connectionManager;
95
96     private String cert;
97     private SSLSocketFactory sslSocketFactory;
98     private final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
99
100         @Override
101         public boolean verify(String arg0, SSLSession arg1) {
102             return arg0.equals(arg1.getPeerHost()) || arg0.contains("dss.local.");
103         }
104     };
105
106     /**
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}.
110      *
111      * @param connectionManager to check connection, can be null
112      * @param exeptAllCerts (true = all will ignore)
113      */
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);
118     }
119
120     /**
121      * Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config} and set ignore all
122      * SSL-Certificates.
123      *
124      * @param config to get configurations, must not be null
125      * @param exeptAllCerts (true = all will ignore)
126      */
127     public HttpTransportImpl(Config config, boolean exeptAllCerts) {
128         this.config = config;
129         init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), exeptAllCerts);
130     }
131
132     /**
133      * Creates a new {@link HttpTransportImpl} with configurations of the given {@link Config}.
134      *
135      * @param config to get configurations, must not be null
136      */
137     public HttpTransportImpl(Config config) {
138         this.config = config;
139         init(config.getHost(), config.getConnectionTimeout(), config.getReadTimeout(), false);
140     }
141
142     /**
143      * Creates a new {@link HttpTransportImpl}.
144      *
145      * @param uri of the server, must not be null
146      */
147     public HttpTransportImpl(String uri) {
148         init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, false);
149     }
150
151     /**
152      * Creates a new {@link HttpTransportImpl} and set ignore all SSL-Certificates.
153      *
154      * @param uri of the server, must not be null
155      * @param exeptAllCerts (true = all will ignore)
156      */
157     public HttpTransportImpl(String uri, boolean exeptAllCerts) {
158         init(uri, Config.DEFAULT_CONNECTION_TIMEOUT, Config.DEFAULT_READ_TIMEOUT, exeptAllCerts);
159     }
160
161     /**
162      * Creates a new {@link HttpTransportImpl}.
163      *
164      * @param uri of the server, must not be null
165      * @param connectTimeout to set
166      * @param readTimeout to set
167      */
168     public HttpTransportImpl(String uri, int connectTimeout, int readTimeout) {
169         init(uri, connectTimeout, readTimeout, false);
170     }
171
172     /**
173      * Creates a new {@link HttpTransportImpl} and set ignore all SSL-Certificates..
174      *
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)
179      */
180     public HttpTransportImpl(String uri, int connectTimeout, int readTimeout, boolean exeptAllCerts) {
181         init(uri, connectTimeout, readTimeout, exeptAllCerts);
182     }
183
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
190         if (exeptAllCerts) {
191             sslSocketFactory = generateSSLContextWhichAcceptAllSSLCertificats();
192         } else {
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);
198                 } else {
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);
204                         }
205                     } else {
206                         logger.debug("generate SSLcontext from server");
207                         cert = getPEMCertificateFromServer(this.uri);
208                         sslSocketFactory = generateSSLContextFromPEMCertString(cert);
209                         if (sslSocketFactory != null) {
210                             config.setCert(cert);
211                         }
212                     }
213                 }
214             } else {
215                 logger.debug("generate SSLcontext from server");
216                 cert = getPEMCertificateFromServer(this.uri);
217                 sslSocketFactory = generateSSLContextFromPEMCertString(cert);
218             }
219         }
220     }
221
222     private String fixURI(String uri) {
223         String fixedURI = uri;
224         if (!fixedURI.startsWith("https://")) {
225             fixedURI = "https://" + fixedURI;
226         }
227         if (fixedURI.split(":").length != 3) {
228             fixedURI = fixedURI + ":8080";
229         }
230         return fixedURI;
231     }
232
233     private String fixRequest(String request) {
234         return request.replace(" ", "");
235     }
236
237     @Override
238     public String execute(String request) {
239         return execute(request, this.connectTimeout, this.readTimeout);
240     }
241
242     private short loginCounter = 0;
243
244     @Override
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
249         // max 1 second.
250         String response = null;
251         HttpsURLConnection connection = null;
252         try {
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());
261                     } else {
262                         response = IOUtils.toString(connection.getInputStream());
263                     }
264                     if (response != null) {
265                         if (!response.contains("Authentication failed")) {
266                             if (loginCounter > 0) {
267                                 connectionManager.checkConnection(responseCode);
268                             }
269                             loginCounter = 0;
270                         } else {
271                             connectionManager.checkConnection(ConnectionManager.AUTHENTIFICATION_PROBLEM);
272                             loginCounter++;
273                         }
274                     }
275
276                 }
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);
283                         loginCounter++;
284                     } else {
285                         connectionManager.checkConnection(responseCode);
286                         loginCounter++;
287                         return null;
288                     }
289                 }
290                 return response;
291             }
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);
305         } finally {
306             if (connection != null) {
307                 connection.disconnect();
308             }
309         }
310         return null;
311     }
312
313     private boolean informConnectionManager(int code) {
314         if (connectionManager != null && loginCounter < MAY_A_NEW_SESSION_TOKEN_IS_NEEDED) {
315             connectionManager.checkConnection(code);
316             return true;
317         }
318         return false;
319     }
320
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());
327                 }
328                 return addSessionToken(request, sessionToken);
329             }
330         }
331         return request;
332     }
333
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);
337     }
338
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;
344             } else {
345                 correctedRequest = correctedRequest + "?" + ParameterKeys.TOKEN + "=" + sessionToken;
346             }
347         } else {
348             correctedRequest = StringUtils.replaceOnce(correctedRequest, StringUtils.substringBefore(
349                     StringUtils.substringAfter(correctedRequest, ParameterKeys.TOKEN + "="), "&"), sessionToken);
350
351         }
352         return correctedRequest;
353     }
354
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);
366                 }
367                 if (hostnameVerifier != null) {
368                     connection.setHostnameVerifier(hostnameVerifier);
369                 }
370             }
371             return connection;
372         }
373         return null;
374     }
375
376     @Override
377     public int checkConnection(String testRequest) {
378         try {
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;
385                     }
386                 }
387                 connection.disconnect();
388                 return connection.getResponseCode();
389             } else {
390                 return ConnectionManager.GENERAL_EXCEPTION;
391             }
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;
402         }
403     }
404
405     @Override
406     public int getSensordataConnectionTimeout() {
407         return config != null ? config.getSensordataConnectionTimeout() : Config.DEFAULT_SENSORDATA_CONNECTION_TIMEOUT;
408     }
409
410     @Override
411     public int getSensordataReadTimeout() {
412         return config != null ? config.getSensordataReadTimeout() : Config.DEFAULT_SENSORDATA_READ_TIMEOUT;
413     }
414
415     private String readPEMCertificateStringFromFile(String path) {
416         if (StringUtils.isBlank(path)) {
417             logger.error("Path is empty.");
418         } else {
419             File dssCert = new File(path);
420             if (dssCert.exists()) {
421                 if (path.endsWith(".crt")) {
422                     try {
423                         InputStream certInputStream = new FileInputStream(dssCert);
424                         String cert = IOUtils.toString(certInputStream);
425                         if (cert.startsWith(BEGIN_CERT)) {
426                             return cert;
427                         } else {
428                             logger.error("File is not a PEM certificate file. PEM-Certificats starts with: {}",
429                                     BEGIN_CERT);
430                         }
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);
435                     }
436                 } else {
437                     logger.error("File is not a certificate (.crt) file.");
438                 }
439             } else {
440                 logger.error("File not found");
441             }
442         }
443         return null;
444     }
445
446     @Override
447     public String writePEMCertFile(String path) {
448         String correctedPath = StringUtils.trimToEmpty(path);
449         File certFilePath;
450         if (StringUtils.isNotBlank(correctedPath)) {
451             certFilePath = new File(correctedPath);
452             boolean pathExists = certFilePath.exists();
453             if (!pathExists) {
454                 pathExists = certFilePath.mkdirs();
455             }
456             if (pathExists && !correctedPath.endsWith("/")) {
457                 correctedPath = correctedPath + "/";
458             }
459         }
460         InputStream certInputStream = IOUtils.toInputStream(cert);
461         X509Certificate trustedCert;
462         try {
463             trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
464                     .generateCertificate(certInputStream);
465
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);
471                 writer.write(cert);
472                 writer.flush();
473                 writer.close();
474                 return certFilePath.getAbsolutePath();
475             } else {
476                 logger.error("File allready exists!");
477             }
478         } catch (IOException e) {
479             logger.error("An IOException occurred: ", e);
480         } catch (CertificateException e1) {
481             logger.error("A CertificateException occurred: ", e1);
482         }
483         return null;
484     }
485
486     private SSLSocketFactory generateSSLContextFromPEMCertString(String pemCert) {
487         if (StringUtils.isNotBlank(pemCert) && pemCert.startsWith(BEGIN_CERT)) {
488             try {
489                 InputStream certInputStream = IOUtils.toInputStream(pemCert);
490                 final X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
491                         .generateCertificate(certInputStream);
492
493                 final TrustManager[] trustManager = new TrustManager[] { new X509TrustManager() {
494
495                     @Override
496                     public java.security.cert.X509Certificate[] getAcceptedIssuers() {
497                         return null;
498                     }
499
500                     @Override
501                     public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
502                             throws CertificateException {
503                         if (!certs[0].equals(trustedCert)) {
504                             throw new CertificateException();
505                         }
506                     }
507
508                     @Override
509                     public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
510                             throws CertificateException {
511                         if (!certs[0].equals(trustedCert)) {
512                             throw new CertificateException();
513                         }
514                     }
515                 } };
516
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);
526             }
527         } else {
528             logger.error("Cert is empty");
529         }
530         return null;
531     }
532
533     private String getPEMCertificateFromServer(String host) {
534         HttpsURLConnection connection = null;
535         try {
536             URL url = new URL(host);
537
538             connection = (HttpsURLConnection) url.openConnection();
539             connection.setHostnameVerifier(hostnameVerifier);
540             connection.setSSLSocketFactory(generateSSLContextWhichAcceptAllSSLCertificats());
541             connection.connect();
542
543             java.security.cert.Certificate[] cert = connection.getServerCertificates();
544             connection.disconnect();
545
546             byte[] by = ((X509Certificate) cert[0]).getEncoded();
547             if (by.length != 0) {
548                 return BEGIN_CERT + Base64.getEncoder().encodeToString(by) + END_CERT;
549             }
550         } catch (MalformedURLException e) {
551             if (!informConnectionManager(ConnectionManager.MALFORMED_URL_EXCEPTION)) {
552                 logger.error("A MalformedURLException occurred: ", e);
553             }
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;
560             }
561             if (!informConnectionManager(code) || code == -1) {
562                 logger.error("An IOException occurred: ", e);
563             }
564         } catch (CertificateEncodingException e) {
565             logger.error("A CertificateEncodingException occurred: ", e);
566         } finally {
567             if (connection != null) {
568                 connection.disconnect();
569             }
570         }
571         return null;
572     }
573
574     private SSLSocketFactory generateSSLContextWhichAcceptAllSSLCertificats() {
575         Security.addProvider(Security.getProvider("SunJCE"));
576         TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
577
578             @Override
579             public java.security.cert.X509Certificate[] getAcceptedIssuers() {
580                 return null;
581             }
582
583             @Override
584             public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
585             }
586
587             @Override
588             public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
589             }
590         } };
591
592         try {
593             SSLContext sslContext = SSLContext.getInstance("SSL");
594
595             sslContext.init(null, trustAllCerts, new SecureRandom());
596
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);
602         }
603         return null;
604     }
605 }