Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[loxone] Support for HTTPS websocket connections. #10185

Merged
merged 1 commit into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions bundles/org.openhab.binding.loxone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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<br>1: Hash-based<br>2: Token-based | 0: Automatic | A method used to authenticate user in the Miniserver. |
| ID | Name | Values | Default | Description |
|-----------------|-----------------------|-------------------------------------------------|--------------|-------------------------------------------------------|
| `authMethod` | Authentication method | 0: Automatic<br>1: Hash-based<br>2: Token-based | 0: Automatic | A method used to authenticate user in the Miniserver. |
| `webSocketType` | WebSocket protocol | 0: Automatic<br>1: Force HTTPS<br>2: Force HTTP | 0: Automatic | Communication protocol used for WebSocket connection. |

### Timeouts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
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;
Expand Down Expand Up @@ -72,7 +73,7 @@
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;

Expand Down Expand Up @@ -139,13 +140,16 @@ public LxServerHandler(Thing thing, LxDynamicStateDescriptionProvider provider)

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("[{}] Handle command: channelUID={}, command={}", debugId, channelUID, command);
if (command instanceof RefreshType) {
updateChannelState(channelUID);
return;
}
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);
Expand Down Expand Up @@ -182,7 +186,7 @@ public void initialize() {
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);
Expand Down Expand Up @@ -478,8 +482,16 @@ private void updateStateValue(LxStateUpdate update) {
Map<LxUuid, LxState> 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());
}
}

Expand Down Expand Up @@ -553,24 +565,51 @@ private boolean connect() {
* 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();

// Following the PR github.com/eclipse/smarthome/pull/6636
// 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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -455,9 +456,20 @@ void sendKeepAlive() {
* @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
Expand Down Expand Up @@ -536,7 +548,7 @@ private boolean sendCmdNoResp(String command, boolean encrypt) {
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);
Expand Down Expand Up @@ -580,7 +592,9 @@ private void processResponse(String message) {
}
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/")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ public final void handleCommand(ChannelUID channelId, Command command) throws IO
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());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class LxResponse {
public class LxResponseCfgApi {
public String snr;
public String version;
public String key;
public Integer httpsStatus;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@
<description>Host address or IP of the Loxone Miniserver</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" groupName="miniserver">
<label>Port</label>
<description>Web interface port of the Loxone Miniserver</description>
<label>HTTP Port</label>
<description>HTTP Web interface port of the Loxone Miniserver</description>
<default>80</default>
</parameter>
<parameter name="httpsPort" type="integer" min="1" max="65535" groupName="miniserver">
<label>HTTPS Port</label>
<description>HTTPS Web interface port of the Loxone Miniserver</description>
<default>443</default>
</parameter>

<parameter name="authMethod" type="integer" min="0" max="2" groupName="security">
<label>Authorization Method</label>
Expand Down Expand Up @@ -71,6 +76,18 @@
<advanced>true</advanced>
</parameter>

<parameter name="webSocketType" type="integer" min="0" max="2" groupName="security">
<label>WebSocket Connection Type</label>
<description>Protocol used to communicate over WebSocket to the Miniserver</description>
<default>0</default>
<options>
<option value="0">Automatic</option>
<option value="1">Force HTTPS</option>
<option value="2">Force HTTP</option>
</options>
<limitToOptions>true</limitToOptions>
<advanced>true</advanced>
</parameter>
<parameter name="firstConDelay" type="integer" min="0" max="120" groupName="timeouts">
<label>First Connection Delay</label>
<description>Time between binding initialization and first connection attempt (seconds, 0-120)</description>
Expand Down