From 3a19b29662c37fb5a37fede04c276acb3fc4ce21 Mon Sep 17 00:00:00 2001 From: Pawel Pieczul Date: Thu, 25 Feb 2021 22:46:04 +0100 Subject: [PATCH] [loxone] Support for HTTPS websocket connections. (#10185) Signed-off-by: Pawel Pieczul --- bundles/org.openhab.binding.loxone/README.md | 10 ++-- .../internal/LxBindingConfiguration.java | 10 +++- .../loxone/internal/LxServerHandler.java | 47 +++++++++++++++++-- .../binding/loxone/internal/LxWebSocket.java | 18 ++++++- .../loxone/internal/controls/LxControl.java | 2 + .../loxone/internal/types/LxResponse.java | 2 + .../resources/OH-INF/thing/thing-types.xml | 21 ++++++++- 7 files changed, 98 insertions(+), 12 deletions(-) diff --git a/bundles/org.openhab.binding.loxone/README.md b/bundles/org.openhab.binding.loxone/README.md index 37911c24b..37ab59094 100644 --- a/bundles/org.openhab.binding.loxone/README.md +++ b/bundles/org.openhab.binding.loxone/README.md @@ -96,6 +96,9 @@ The acquired token will remain active for several weeks following the last succe In case a websocket connection to the Miniserver remains active for the whole duration of the token's life span, the binding will refresh the token one day before token expiration, without the need of providing the password. +In case of connecting to Generation 2 Miniservers, it is possible to establish a secure WebSocket connection over HTTPS protocol. Binding will automatically detect if HTTPS connection is available and will use it. In that case, commands sent to the Miniserver will not be additionally encrypted. When HTTPS is not available, binding will use unsecure HTTP connection and will encrypt each command. + +It is possible to override the communication protocol by setting `webSocketType` configuration parameter. Setting it to 1 will force to always establish HTTPS connection. Setting it to 2 will force to always establish HTTP connection. Default value of 0 means the binding will determine the right protocol in the runtime. A method to enable unrestricted security policy depends on the JRE version and vendor, some examples can be found [here](https://www.petefreitag.com/item/844.cfm) and [here](https://stackoverflow.com/questions/41580489/how-to-install-unlimited-strength-jurisdiction-policy-files). @@ -195,9 +198,10 @@ To define a parameter value in a .things file, please refer to it by parameter's ### Security -| ID | Name | Values | Default | Description | -|--------------|-----------------------|-------------------------------------------------|--------------|-------------------------------------------------------| -| `authMethod` | Authentication method | 0: Automatic
1: Hash-based
2: Token-based | 0: Automatic | A method used to authenticate user in the Miniserver. | +| ID | Name | Values | Default | Description | +|-----------------|-----------------------|-------------------------------------------------|--------------|-------------------------------------------------------| +| `authMethod` | Authentication method | 0: Automatic
1: Hash-based
2: Token-based | 0: Automatic | A method used to authenticate user in the Miniserver. | +| `webSocketType` | WebSocket protocol | 0: Automatic
1: Force HTTPS
2: Force HTTP | 0: Automatic | Communication protocol used for WebSocket connection. | ### Timeouts diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java index 1cebb8770..5d817bd2f 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxBindingConfiguration.java @@ -24,9 +24,13 @@ public class LxBindingConfiguration { */ public String host; /** - * Port of web service of the Miniserver + * Port of HTTP web service of the Miniserver */ public int port; + /** + * Port of HTTPS web service of the Miniserver + */ + public int httpsPort; /** * User name used to log into the Miniserver */ @@ -76,4 +80,8 @@ public class LxBindingConfiguration { * Authentication method (0-auto, 1-hash, 2-token) */ public int authMethod; + /** + * WebSocket connection type (0-auto, 1-HTTPS, 2-HTTP) + */ + public int webSocketType; } diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java index aa6b9d58d..17517e2b3 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxServerHandler.java @@ -32,6 +32,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; @@ -72,7 +73,7 @@ import com.google.gson.GsonBuilder; public class LxServerHandler extends BaseThingHandler implements LxServerHandlerApi { private static final String SOCKET_URL = "/ws/rfc6455"; - private static final String CMD_CFG_API = "jdev/cfg/api"; + private static final String CMD_CFG_API = "jdev/cfg/apiKey"; private static final Gson GSON; @@ -139,6 +140,7 @@ public class LxServerHandler extends BaseThingHandler implements LxServerHandler @Override public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("[{}] Handle command: channelUID={}, command={}", debugId, channelUID, command); if (command instanceof RefreshType) { updateChannelState(channelUID); return; @@ -146,6 +148,8 @@ public class LxServerHandler extends BaseThingHandler implements LxServerHandler try { LxControl control = channels.get(channelUID); if (control != null) { + logger.debug("[{}] Dispatching command to control UUID={}, name={}", debugId, control.getUuid(), + control.getName()); control.handleCommand(channelUID, command); } else { logger.error("[{}] Received command {} for unknown control.", debugId, command); @@ -182,7 +186,7 @@ public class LxServerHandler extends BaseThingHandler implements LxServerHandler jettyThreadPool.setDaemon(true); socket = new LxWebSocket(debugId, this, bindingConfig, host); - wsClient = new WebSocketClient(); + wsClient = new WebSocketClient(new SslContextFactory.Client(true)); wsClient.setExecutor(jettyThreadPool); if (debugId > 1) { reconnectDelay.set(0); @@ -478,8 +482,16 @@ public class LxServerHandler extends BaseThingHandler implements LxServerHandler Map perStateUuid = states.get(update.getUuid()); if (perStateUuid != null) { perStateUuid.forEach((controlUuid, state) -> { + logger.debug("[{}] State update (UUID={}, value={}) dispatched to control UUID={}, state name={}", + debugId, update.getUuid(), update.getValue(), controlUuid, state.getName()); + state.setStateValue(update.getValue()); }); + if (perStateUuid.size() == 0) { + logger.debug("[{}] State update UUID={} has empty controls table", debugId, update.getUuid()); + } + } else { + logger.debug("[{}] State update UUID={} has no controls table", debugId, update.getUuid()); } } @@ -553,16 +565,36 @@ public class LxServerHandler extends BaseThingHandler implements LxServerHandler * Try to read CfgApi structure from the miniserver. It contains serial number and firmware version. If it can't * be read this is not a fatal issue, we will assume most recent version running. */ + boolean httpsCapable = false; String message = socket.httpGet(CMD_CFG_API); if (message != null) { LxResponse resp = socket.getResponse(message); if (resp != null) { - socket.setFwVersion(GSON.fromJson(resp.getValueAsString(), LxResponse.LxResponseCfgApi.class).version); + LxResponse.LxResponseCfgApi apiResp = GSON.fromJson(resp.getValueAsString(), + LxResponse.LxResponseCfgApi.class); + if (apiResp != null) { + socket.setFwVersion(apiResp.version); + httpsCapable = apiResp.httpsStatus != null && apiResp.httpsStatus == 1; + } } } else { logger.debug("[{}] Http get failed for API config request.", debugId); } + switch (bindingConfig.webSocketType) { + case 0: + // keep automatically determined option + break; + case 1: + logger.debug("[{}] Forcing HTTPS websocket connection.", debugId); + httpsCapable = true; + break; + case 2: + logger.debug("[{}] Forcing HTTP websocket connection.", debugId); + httpsCapable = false; + break; + } + try { wsClient.start(); @@ -570,7 +602,14 @@ public class LxServerHandler extends BaseThingHandler implements LxServerHandler // without this zero timeout, jetty will wait 30 seconds for stopping the client to eventually fail // with the timeout it is immediate and all threads end correctly jettyThreadPool.setStopTimeout(0); - URI target = new URI("ws://" + host.getHostAddress() + ":" + bindingConfig.port + SOCKET_URL); + URI target; + if (httpsCapable) { + target = new URI("wss://" + host.getHostAddress() + ":" + bindingConfig.httpsPort + SOCKET_URL); + socket.setHttps(true); + } else { + target = new URI("ws://" + host.getHostAddress() + ":" + bindingConfig.port + SOCKET_URL); + socket.setHttps(false); + } ClientUpgradeRequest request = new ClientUpgradeRequest(); request.setSubProtocols("remotecontrol"); diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java index 79867bd91..298e53f15 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/LxWebSocket.java @@ -78,6 +78,7 @@ public class LxWebSocket { private Session session; private String fwVersion; + private boolean httpsSession = false; private ScheduledFuture timeout; private LxWsBinaryHeader header; private LxWsSecurity security; @@ -455,9 +456,20 @@ public class LxWebSocket { * @param fwVersion Miniserver firmware version */ void setFwVersion(String fwVersion) { + logger.debug("[{}] Firmware version: {}", debugId, fwVersion); this.fwVersion = fwVersion; } + /** + * Sets information if session is over HTTPS or HTTP protocol + * + * @param httpsSession true when HTTPS session + */ + void setHttps(boolean httpsSession) { + logger.debug("[{}] HTTPS session: {}", debugId, httpsSession); + this.httpsSession = httpsSession; + } + /** * Start a timer to wait for a Miniserver response to an action sent from the binding. * When timer expires, connection is removed and server error is reported. Further connection attempt can be made @@ -536,7 +548,7 @@ public class LxWebSocket { try { if (session != null) { String encrypted; - if (encrypt) { + if (encrypt && !httpsSession) { encrypted = security.encrypt(command); logger.debug("[{}] Sending encrypted string: {}", debugId, command); logger.debug("[{}] Encrypted: {}", debugId, encrypted); @@ -580,7 +592,9 @@ public class LxWebSocket { } logger.debug("[{}] Response: {}", debugId, message.trim()); String control = resp.getCommand().trim(); - control = security.decryptControl(control); + if (!httpsSession) { + control = security.decryptControl(control); + } // for some reason the responses to some commands starting with jdev begin with dev, not jdev // this seems to be a bug in the Miniserver if (control.startsWith("dev/")) { diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java index 2533b90f8..f16c04596 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java @@ -264,6 +264,8 @@ public class LxControl { Callbacks c = callbacks.get(channelId); if (c != null && c.commandCallback != null) { c.commandCallback.handleCommand(command); + } else { + logger.debug("Control UUID={} has no command handler", getUuid()); } } diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java index 45af9c7da..c9e4a37c6 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/types/LxResponse.java @@ -39,6 +39,8 @@ public class LxResponse { public class LxResponseCfgApi { public String snr; public String version; + public String key; + public Integer httpsStatus; } /** diff --git a/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml index 52ef9ccf6..72330526c 100644 --- a/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.loxone/src/main/resources/OH-INF/thing/thing-types.xml @@ -39,10 +39,15 @@ Host address or IP of the Loxone Miniserver - - Web interface port of the Loxone Miniserver + + HTTP Web interface port of the Loxone Miniserver 80 + + + HTTPS Web interface port of the Loxone Miniserver + 443 + @@ -71,6 +76,18 @@ true + + + Protocol used to communicate over WebSocket to the Miniserver + 0 + + + + + + true + true + Time between binding initialization and first connection attempt (seconds, 0-120)