From 7f244186ff7001e4eb27ae4895dec4eb0f91f135 Mon Sep 17 00:00:00 2001 From: pali Date: Sun, 20 Jun 2021 21:07:12 +0300 Subject: [PATCH] [ihc] Support for TLSv1.2 (#10856) * [ihc] Support for TLSv1.2 So far IHC controller have supported only TLSv1. As TLSv1 is already deprecated and upcoming IHC controller FW version will introduce support for TLSv1.2. This change introduce TLSv1.2 support for the binding. TLS version is selectable to be backward compatible for older controller versions. Communication error handling is also improved e.g. because of wrong password or if Java environment doesn't support required TLS version. When fatal communication error occurs, binding will not hammer controller with retries as it need user actions. Signed-off-by: Pauli Anttila * [ihc] Introduced auto mode and other improvements Signed-off-by: Pauli Anttila * [ihc] introduced default constant Signed-off-by: Pauli Anttila --- bundles/org.openhab.binding.ihc/README.md | 19 +-- .../ihc/internal/config/IhcConfiguration.java | 3 +- .../ihc/internal/handler/IhcHandler.java | 8 +- .../binding/ihc/internal/ws/IhcClient.java | 37 ++++-- .../ws/exeptions/IhcFatalExecption.java | 38 ++++++ .../ws/exeptions/IhcTlsExecption.java | 38 ++++++ .../internal/ws/http/IhcConnectionPool.java | 121 ++++++++++-------- .../ihc/internal/ws/http/IhcHttpsClient.java | 5 + .../resources/OH-INF/thing/controller.xml | 11 ++ 9 files changed, 207 insertions(+), 73 deletions(-) create mode 100644 bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcFatalExecption.java create mode 100644 bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcTlsExecption.java diff --git a/bundles/org.openhab.binding.ihc/README.md b/bundles/org.openhab.binding.ihc/README.md index 5d352ec4039ea..803f73fd081b9 100644 --- a/bundles/org.openhab.binding.ihc/README.md +++ b/bundles/org.openhab.binding.ihc/README.md @@ -20,14 +20,15 @@ This binding supports one ThingType: `controller`. The `controller` Thing has the following configuration parameters: -| Parameter | Description | Required | Default value | -|-----------------------------|------------------------------------------------------------------------------------------------------------------------------|----------|---------------| -| hostname | Network/IP address of the IHC / ELKO controller without https prefix, but can contain TCP port if default port is not used. | yes | | -| username | User name to login to the IHC / ELKO controller. | yes | | -| password | Password to login to the IHC / ELKO controller. | yes | | -| timeout | Timeout in milliseconds to communicate to IHC / ELKO controller. | no | 5000 | -| loadProjectFile | Load project file from controller. | no | true | -| createChannelsAutomatically | Create channels automatically from project file. Project file loading parameter should be enabled as well. | no | true | +| Parameter | Description | Required | Default value | +|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| +| hostname | Network/IP address of the IHC / ELKO controller without https prefix, but can contain TCP port if default port is not used. | yes | | +| username | User name to login to the IHC / ELKO controller. | yes | | +| password | Password to login to the IHC / ELKO controller. | yes | | +| timeout | Timeout in milliseconds to communicate to IHC / ELKO controller. | no | 5000 | +| loadProjectFile | Load project file from controller. | no | true | +| createChannelsAutomatically | Create channels automatically from project file. Project file loading parameter should be enabled as well. | no | true | +| tlsVersion | TLS version used for controller communication. Choose `TLSv1` for older firmware versions and `TLSv1.2` for never versions (since fall 2021). `AUTO` mode try to recognize correct version. | no | TLSv1 | ## Channels @@ -148,7 +149,7 @@ Will send TOGGLE (ON/OFF) command to Dimmer test item when short button press is ### example.things ```xtend -ihc:controller:elko [ hostname="192.168.1.2", username="openhab", password="secret", timeout=5000, loadProjectFile=true, createChannelsAutomatically=false ] { +ihc:controller:elko [ hostname="192.168.1.2", username="openhab", password="secret", timeout=5000, loadProjectFile=true, createChannelsAutomatically=false, tlsVersion="TLSv1" ] { Channels: Type switch : my_test_switch "My Test Switch" [ resourceId=3988827 ] Type contact : my_test_contact "My Test Contact" [ resourceId=3988827 ] diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/config/IhcConfiguration.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/config/IhcConfiguration.java index da681b88f9f13..5aee66193a350 100644 --- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/config/IhcConfiguration.java +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/config/IhcConfiguration.java @@ -24,11 +24,12 @@ public class IhcConfiguration { public int timeout; public boolean loadProjectFile; public boolean createChannelsAutomatically; + public String tlsVersion; @Override public String toString() { return "[" + "hostname=" + hostname + ", username=" + username + ", password=******" + ", timeout=" + timeout + ", loadProjectFile=" + loadProjectFile + ", createChannelsAutomatically=" - + createChannelsAutomatically + "]"; + + createChannelsAutomatically + ", tlsVersion=" + tlsVersion + "]"; } } diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/handler/IhcHandler.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/handler/IhcHandler.java index 220ec92eb177c..9c7d36ea1e30b 100644 --- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/handler/IhcHandler.java +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/handler/IhcHandler.java @@ -50,6 +50,7 @@ import org.openhab.binding.ihc.internal.ws.datatypes.WSTimeManagerSettings; import org.openhab.binding.ihc.internal.ws.exeptions.ConversionException; import org.openhab.binding.ihc.internal.ws.exeptions.IhcExecption; +import org.openhab.binding.ihc.internal.ws.exeptions.IhcFatalExecption; import org.openhab.binding.ihc.internal.ws.projectfile.IhcEnumValue; import org.openhab.binding.ihc.internal.ws.projectfile.ProjectFileUtils; import org.openhab.binding.ihc.internal.ws.resourcevalues.WSBooleanValue; @@ -534,7 +535,7 @@ private void connect() throws IhcExecption { setConnectingState(true); logger.debug("Connecting to IHC / ELKO LS controller [hostname='{}', username='{}'].", conf.hostname, conf.username); - ihc = new IhcClient(conf.hostname, conf.username, conf.password, conf.timeout); + ihc = new IhcClient(conf.hostname, conf.username, conf.password, conf.timeout, conf.tlsVersion); ihc.openConnection(); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Initializing communication to the IHC / ELKO controller"); @@ -883,6 +884,11 @@ private void reconnectCheck() { } connect(); setReconnectRequest(false); + } catch (IhcFatalExecption e) { + logger.warn("Can't open connection to controller {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + setReconnectRequest(false); + return; } catch (IhcExecption e) { logger.debug("Can't open connection to controller {}", e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java index a1e384f9e251f..76750c9a23019 100644 --- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/IhcClient.java @@ -35,6 +35,8 @@ import org.openhab.binding.ihc.internal.ws.datatypes.WSSystemInfo; import org.openhab.binding.ihc.internal.ws.datatypes.WSTimeManagerSettings; import org.openhab.binding.ihc.internal.ws.exeptions.IhcExecption; +import org.openhab.binding.ihc.internal.ws.exeptions.IhcFatalExecption; +import org.openhab.binding.ihc.internal.ws.exeptions.IhcTlsExecption; import org.openhab.binding.ihc.internal.ws.http.IhcConnectionPool; import org.openhab.binding.ihc.internal.ws.resourcevalues.WSResourceValue; import org.openhab.binding.ihc.internal.ws.services.IhcAirlinkManagementService; @@ -60,6 +62,10 @@ public enum ConnectionState { CONNECTED } + public static final String TLS_VER_AUTO = "AUTO"; + public static final String TLS_VER_V1 = "TLSv1"; + public static final String TLS_VER_V1_2 = "TLSv1.2"; + public static final String CONTROLLER_STATE_READY = "text.ctrl.state.ready"; public static final String CONTROLLER_STATE_INITIALIZE = "text.ctrl.state.initialize"; @@ -88,6 +94,7 @@ public enum ConnectionState { private String username; private String password; private String host; + private String tlsVersion; /** Timeout in milliseconds */ private int timeout; @@ -96,14 +103,15 @@ public enum ConnectionState { private List eventListeners = new ArrayList<>(); public IhcClient(String host, String username, String password) { - this(host, username, password, 5000); + this(host, username, password, 5000, TLS_VER_V1); } - public IhcClient(String host, String username, String password, int timeout) { + public IhcClient(String host, String username, String password, int timeout, String tlsVersion) { this.host = host; this.username = username; this.password = password; this.timeout = timeout; + this.tlsVersion = tlsVersion; } public synchronized ConnectionState getConnectionState() { @@ -168,10 +176,23 @@ public void closeConnection() throws IhcExecption { * @throws IhcExecption */ public void openConnection() throws IhcExecption { - logger.debug("Opening connection"); + if (TLS_VER_AUTO.equalsIgnoreCase(tlsVersion)) { + try { + openConnection(TLS_VER_V1); + } catch (IhcTlsExecption e) { + logger.debug("Connection failed with TLS {}, trying with TLS {}", TLS_VER_V1, TLS_VER_V1_2); + openConnection(TLS_VER_V1_2); + } + } else { + openConnection(tlsVersion); + } + } + + private void openConnection(String tlsVersion) throws IhcExecption { + logger.debug("Opening connection with TLS version {}", tlsVersion); setConnectionState(ConnectionState.CONNECTING); - ihcConnectionPool = new IhcConnectionPool(); + ihcConnectionPool = new IhcConnectionPool(tlsVersion); authenticationService = new IhcAuthenticationService(host, timeout, ihcConnectionPool); WSLoginResult loginResult = authenticationService.authenticate(username, password, "treeview"); @@ -181,18 +202,18 @@ public void openConnection() throws IhcExecption { setConnectionState(ConnectionState.DISCONNECTED); if (loginResult.isLoginFailedDueToAccountInvalid()) { - throw new IhcExecption("login failed because of invalid account"); + throw new IhcFatalExecption("login failed because of invalid account"); } if (loginResult.isLoginFailedDueToConnectionRestrictions()) { - throw new IhcExecption("login failed because of connection restrictions"); + throw new IhcFatalExecption("login failed because of connection restrictions"); } if (loginResult.isLoginFailedDueToInsufficientUserRights()) { - throw new IhcExecption("login failed because of insufficient user rights"); + throw new IhcFatalExecption("login failed because of insufficient user rights"); } - throw new IhcExecption("login failed because of unknown reason"); + throw new IhcFatalExecption("login failed because of unknown reason"); } logger.debug("Connection successfully opened"); diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcFatalExecption.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcFatalExecption.java new file mode 100644 index 0000000000000..b07f2ca93179a --- /dev/null +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcFatalExecption.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ihc.internal.ws.exeptions; + +/** + * Exception for handling fatal communication errors to controller. + * + * @author Pauli Anttila - Initial contribution + */ +public class IhcFatalExecption extends IhcExecption { + + private static final long serialVersionUID = -8107948791816894352L; + + public IhcFatalExecption() { + } + + public IhcFatalExecption(String message) { + super(message); + } + + public IhcFatalExecption(String message, Throwable cause) { + super(message, cause); + } + + public IhcFatalExecption(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcTlsExecption.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcTlsExecption.java new file mode 100644 index 0000000000000..fee8d2eee53db --- /dev/null +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/exeptions/IhcTlsExecption.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ihc.internal.ws.exeptions; + +/** + * Exception for handling TLS communication errors to controller. + * + * @author Pauli Anttila - Initial contribution + */ +public class IhcTlsExecption extends IhcFatalExecption { + + private static final long serialVersionUID = -1366186910684967044L; + + public IhcTlsExecption() { + } + + public IhcTlsExecption(String message) { + super(message); + } + + public IhcTlsExecption(String message, Throwable cause) { + super(message, cause); + } + + public IhcTlsExecption(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcConnectionPool.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcConnectionPool.java index 5f6181dfca607..da2feba1e306d 100644 --- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcConnectionPool.java +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcConnectionPool.java @@ -33,6 +33,8 @@ import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.openhab.binding.ihc.internal.ws.exeptions.IhcFatalExecption; +import org.openhab.binding.ihc.internal.ws.exeptions.IhcTlsExecption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +46,7 @@ public class IhcConnectionPool { private final Logger logger = LoggerFactory.getLogger(IhcConnectionPool.class); + private static final String DEFAULT_TLS_VER = "TLSv1"; /** * Controller TLS certificate is self signed, which means that certificate @@ -58,83 +61,93 @@ public class IhcConnectionPool { private HttpClientBuilder httpClientBuilder; private HttpClientContext localContext; + private String tlsVersion = DEFAULT_TLS_VER; - public IhcConnectionPool() { + public IhcConnectionPool() throws IhcFatalExecption { + this(DEFAULT_TLS_VER); + } + + public IhcConnectionPool(String tlsVersion) throws IhcFatalExecption { + if (!tlsVersion.isEmpty()) { + this.tlsVersion = tlsVersion; + } init(); } - private void init() { - // Create a local instance of cookie store - cookieStore = new BasicCookieStore(); + private void init() throws IhcFatalExecption { + try { - // Create local HTTP context - localContext = HttpClientContext.create(); + // Create a local instance of cookie store + cookieStore = new BasicCookieStore(); - // Bind custom cookie store to the local context - localContext.setCookieStore(cookieStore); + // Create local HTTP context + localContext = HttpClientContext.create(); - httpClientBuilder = HttpClientBuilder.create(); + // Bind custom cookie store to the local context + localContext.setCookieStore(cookieStore); - // Setup a Trust Strategy that allows all certificates. + httpClientBuilder = HttpClientBuilder.create(); - logger.debug("Initialize SSL context"); + // Setup a Trust Strategy that allows all certificates. - // Create a trust manager that does not validate certificate chains, but accept all. - TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { + logger.debug("Initialize SSL context"); - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; - } + // Create a trust manager that does not validate certificate chains, but accept all. + TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { - @Override - public void checkClientTrusted(X509Certificate[] certs, String authType) { - } + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } - @Override - public void checkServerTrusted(X509Certificate[] certs, String authType) { - logger.trace("Trusting server cert: {}", certs[0].getIssuerDN()); - } - } }; + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } - // Install the all-trusting trust manager + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + logger.trace("Trusting server cert: {}", certs[0].getIssuerDN()); + } + } }; - try { - // Controller supports only SSLv3 and TLSv1 - sslContext = SSLContext.getInstance("TLSv1"); + // Install the all-trusting trust manager + + // Old controller FW supports only SSLv3 and TLSv1, never controller TLSv1.2 + sslContext = SSLContext.getInstance(tlsVersion); + logger.debug("Using TLS version {}", sslContext.getProtocol()); sslContext.init(null, trustAllCerts, new SecureRandom()); - } catch (NoSuchAlgorithmException e) { - logger.warn("Exception", e); - } catch (KeyManagementException e) { - logger.warn("Exception", e); - } - // Controller accepts only HTTPS connections and because normally IP address are used on home network rather - // than DNS names, create custom host name verifier. - HostnameVerifier hostnameVerifier = new HostnameVerifier() { + // Controller accepts only HTTPS connections and because normally IP address are used on home network rather + // than DNS names, create custom host name verifier. + HostnameVerifier hostnameVerifier = new HostnameVerifier() { - @Override - public boolean verify(String arg0, SSLSession arg1) { - logger.trace("HostnameVerifier: arg0 = {}, arg1 = {}", arg0, arg1); - return true; - } - }; + @Override + public boolean verify(String arg0, SSLSession arg1) { + logger.trace("HostnameVerifier: arg0 = {}, arg1 = {}", arg0, arg1); + return true; + } + }; - // Create an SSL Socket Factory, to use our weakened "trust strategy" - SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, - new String[] { "TLSv1" }, null, hostnameVerifier); + // Create an SSL Socket Factory, to use our weakened "trust strategy" + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, + new String[] { tlsVersion }, null, hostnameVerifier); - Registry socketFactoryRegistry = RegistryBuilder. create() - .register("https", sslSocketFactory).build(); + Registry socketFactoryRegistry = RegistryBuilder. create() + .register("https", sslSocketFactory).build(); - // Create connection-manager using our Registry. Allows multi-threaded use - PoolingHttpClientConnectionManager connMngr = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + // Create connection-manager using our Registry. Allows multi-threaded use + PoolingHttpClientConnectionManager connMngr = new PoolingHttpClientConnectionManager(socketFactoryRegistry); - // Increase max connection counts - connMngr.setMaxTotal(20); - connMngr.setDefaultMaxPerRoute(6); + // Increase max connection counts + connMngr.setMaxTotal(20); + connMngr.setDefaultMaxPerRoute(6); - httpClientBuilder.setConnectionManager(connMngr); + httpClientBuilder.setConnectionManager(connMngr); + } catch (KeyManagementException e) { + throw new IhcFatalExecption(e); + } catch (NoSuchAlgorithmException e) { + throw new IhcTlsExecption(e); + } } public HttpClient getHttpClient() { diff --git a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcHttpsClient.java b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcHttpsClient.java index 76bd2cedef67a..e6741b1034c14 100644 --- a/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcHttpsClient.java +++ b/bundles/org.openhab.binding.ihc/src/main/java/org/openhab/binding/ihc/internal/ws/http/IhcHttpsClient.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import javax.net.ssl.SSLHandshakeException; + import org.apache.http.HttpResponse; import org.apache.http.NoHttpResponseException; import org.apache.http.client.ClientProtocolException; @@ -29,6 +31,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.openhab.binding.ihc.internal.ws.exeptions.IhcExecption; +import org.openhab.binding.ihc.internal.ws.exeptions.IhcTlsExecption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,6 +75,8 @@ public String sendQuery(String url, Map requestProperties, Strin setRequestProperty(requestProperties); try { return sendQ(query, timeout); + } catch (SSLHandshakeException e) { + throw new IhcTlsExecption(e); } catch (NoHttpResponseException | SocketTimeoutException e) { try { logger.debug("No response received, resend query"); diff --git a/bundles/org.openhab.binding.ihc/src/main/resources/OH-INF/thing/controller.xml b/bundles/org.openhab.binding.ihc/src/main/resources/OH-INF/thing/controller.xml index 403549b8a39bc..74793ac6cf648 100644 --- a/bundles/org.openhab.binding.ihc/src/main/resources/OH-INF/thing/controller.xml +++ b/bundles/org.openhab.binding.ihc/src/main/resources/OH-INF/thing/controller.xml @@ -59,6 +59,17 @@ well. true + + + TLS version used for controller communication. Choose TLSv1 for older firmware versions and TLSv1.2 for + never versions (since fall 2021). Auto mode try to recognize correct version. + TLSv1 + + + + + +