From 61280ea4a25ecf8843b5c3f929a7743bb42ef902 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 | 151 ++++++++++-------- .../ihc/internal/ws/http/IhcHttpsClient.java | 5 + .../resources/OH-INF/thing/controller.xml | 11 ++ 9 files changed, 222 insertions(+), 88 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 5d352ec40..803f73fd0 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 da681b88f..5aee66193 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 220ec92eb..9c7d36ea1 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.WSSystemInfo; 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 @@ public class IhcHandler extends BaseThingHandler implements IhcEventListener { 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 @@ public class IhcHandler extends BaseThingHandler implements IhcEventListener { } 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 a1e384f9e..76750c9a2 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.WSRFDevice; 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 class IhcClient { 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 class IhcClient { private String username; private String password; private String host; + private String tlsVersion; /** Timeout in milliseconds */ private int timeout; @@ -96,14 +103,15 @@ public class IhcClient { 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 class IhcClient { * @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 class IhcClient { 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 000000000..b07f2ca93 --- /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 000000000..fee8d2eee --- /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 5f6181dfc..da2feba1e 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.conn.ssl.SSLConnectionSocketFactory; 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 @@ import org.slf4j.LoggerFactory; 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(); - - // Create local HTTP context - localContext = HttpClientContext.create(); - - // Bind custom cookie store to the local context - localContext.setCookieStore(cookieStore); - - httpClientBuilder = HttpClientBuilder.create(); - - // Setup a Trust Strategy that allows all certificates. - - logger.debug("Initialize SSL context"); - - // Create a trust manager that does not validate certificate chains, but accept all. - TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { - - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; - } - - @Override - public void checkClientTrusted(X509Certificate[] certs, String authType) { - } - - @Override - public void checkServerTrusted(X509Certificate[] certs, String authType) { - logger.trace("Trusting server cert: {}", certs[0].getIssuerDN()); - } - } }; - - // Install the all-trusting trust manager - + private void init() throws IhcFatalExecption { try { - // Controller supports only SSLv3 and TLSv1 - sslContext = SSLContext.getInstance("TLSv1"); + + // Create a local instance of cookie store + cookieStore = new BasicCookieStore(); + + // Create local HTTP context + localContext = HttpClientContext.create(); + + // Bind custom cookie store to the local context + localContext.setCookieStore(cookieStore); + + httpClientBuilder = HttpClientBuilder.create(); + + // Setup a Trust Strategy that allows all certificates. + + logger.debug("Initialize SSL context"); + + // Create a trust manager that does not validate certificate chains, but accept all. + TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + logger.trace("Trusting server cert: {}", certs[0].getIssuerDN()); + } + } }; + + // 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); + + // 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; + } + }; + + // 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(); + + // 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); + + httpClientBuilder.setConnectionManager(connMngr); } catch (KeyManagementException e) { - logger.warn("Exception", e); + throw new IhcFatalExecption(e); + } catch (NoSuchAlgorithmException e) { + throw new IhcTlsExecption(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() { - - @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); - - Registry socketFactoryRegistry = RegistryBuilder. create() - .register("https", sslSocketFactory).build(); - - // 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); - - httpClientBuilder.setConnectionManager(connMngr); } 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 76bd2cede..e6741b103 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.time.LocalDateTime; 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.client.methods.HttpPost; 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 abstract class IhcHttpsClient { 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 403549b8a..74793ac6c 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 + + + + + +