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 + + + + + +