[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 <pauli.anttila@gmail.com>

* [ihc] Introduced auto mode and other improvements

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>

* [ihc] introduced default constant

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
This commit is contained in:
pali 2021-06-20 21:07:12 +03:00 committed by GitHub
parent 3d33ee0ab8
commit 61280ea4a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 222 additions and 88 deletions

View File

@ -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 ]

View File

@ -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 + "]";
}
}

View File

@ -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());

View File

@ -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<IhcEventListener> 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");

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> 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<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> 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() {

View File

@ -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");

View File

@ -59,6 +59,17 @@
well.</description>
<default>true</default>
</parameter>
<parameter name="tlsVersion" type="text" required="false">
<label>TLS version</label>
<description>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.</description>
<default>TLSv1</default>
<options>
<option value="TLSv1">TLSv1</option>
<option value="TLSv1.2">TLSv1.2</option>
<option value="AUTO">Auto</option>
</options>
</parameter>
</config-description>
</thing-type>