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

[homematic] Add Authentication #16196

Merged
merged 3 commits into from
Feb 19, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 24 additions & 2 deletions bundles/org.openhab.binding.homematic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,28 @@ When the option "Restricted access" is used, some ports have to be added to the
2000;
2001;
2010;
8181;
8701;
9292;
```

Also the IP of the device running openHAB has to be set to the list of "IP addresses for restricted access".

Also under `Home page > Settings > Control panel` with the menu `Security` the option `Authentication` has to be disabled as the binding does not support the configuration of `username` and `password`for the XML-RPC API.
Also under `Home page > Settings > Control panel` with the menu `Security` the option `Authentication` has to be disabled if the option 'useAuthentication' is not set.
This option may be enabled if the option 'useAuthentication' is set and BIN-RPC is not used.
In this case, a user and password must be created.
This can be done under `Home page > Settings > Control panel` with the menu `User management`.
This can be done under `Home page > Settings > Control Panel` in the `User Management` menu.
The new user should have the following configuration:

If this is not done the binding will not be able to connect to the CCU and the CCU Thing will stay uninitialized and sets a timeout exception:
- User name - button for login: No
EvilPingu marked this conversation as resolved.
Show resolved Hide resolved
- Permission level: User
- Expert mode not visible: Yes
- Automatically confirm the device message: Yes

The user and password must then be entered in the 'Username' and 'Password' settings.

If this is not done the binding will not be able to connect to the CCU and the CCU Thing will stay uninitialized and sets a timeout exception or a authentication error

```text
xxx-xx-xx xx:xx:xx.xxx [hingStatusInfoChangedEvent] - - 'homematic:bridge:xxx' changed from INITIALIZING to OFFLINE (COMMUNICATION_ERROR): java.net.SocketTimeoutException: Connect Timeout
Expand Down Expand Up @@ -179,6 +192,15 @@ Due to the factory reset, the device will also be unpaired from the gateway, eve
If a large number of devices are connected to the gateway, the default buffersize of 2048 kB may be too small for communication with the gateway.
In this case, e.g. the discovery fails.
With this setting the buffer size can be adjusted. The value is specified in kB.

- **useAuthentication**
Username and password are send to the gateway to authenticate the access to the gateway.

- **username**
Username for Authentication to the gateway.

- **password**
Password for Authentication to the gateway.

The syntax for a bridge is:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2024 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.homematic.internal.common;

import java.util.Base64;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.openhab.core.i18n.ConfigurationException;

/**
* Handles the authentication to Homematic server.
*
* @author Christian Kittel
*/
@NonNullByDefault
public class AuthenticationHandler {

private Boolean useAuthentication;
private @Nullable String authValue;

public AuthenticationHandler(HomematicConfig config) throws ConfigurationException {
this.useAuthentication = config.getUseAuthentication();
if (!useAuthentication) {
return;
}

if (config.getPassword() == null || config.getUserName() == null) {
throw new ConfigurationException("Username or password missing");
}
this.authValue = "Basic "
+ Base64.getEncoder().encodeToString((config.getUserName() + ":" + config.getPassword()).getBytes());
}

/**
* Add or remove the basic auth credentials th the request if needed.
*/
public Request updateAuthenticationInformation(final Request request) {
return useAuthentication ? request.header(HttpHeader.AUTHORIZATION, authValue) : request;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public class HomematicConfig {
private HmGatewayInfo gatewayInfo;
private int callbackRegTimeout;

private boolean useAuthentication;
private String userName;
private String password;

/**
* Returns the Homematic gateway address.
*/
Expand Down Expand Up @@ -272,6 +276,48 @@ public int getRpcPort(HmChannel channel) {
return getRpcPort(channel.getDevice().getHmInterface());
}

/**
* Returns true if authorization for the gateway has to be used; otherwise false
*/
public boolean getUseAuthentication() {
return useAuthentication;
}

/**
* Sets if authorization for the gateway has to be used
*/
public void setUseAuthentication(Boolean useAuthentication) {
this.useAuthentication = useAuthentication;
}

/**
* Returns the user name for authorize against the gateway
*/
public String getUserName() {
return userName;
}

/**
* Sets the user name for authorize against the gateway
*/
public void setUserName(String userName) {
this.userName = userName;
}

/**
* Returns the password for authorize against the gateway
*/
public String getPassword() {
return password;
}

/**
* Sets the password for authorize against the gateway
*/
public void setPassword(String password) {
this.password = password;
}

/**
* Returns the Homematic gateway port of the interfaces.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/
package org.openhab.binding.homematic.internal.communicator;

import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
import static org.openhab.binding.homematic.internal.HomematicBindingConstants.GATEWAY_POOL_NAME;
import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;

import java.io.IOException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.openhab.binding.homematic.internal.common.AuthenticationHandler;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.client.UnknownParameterSetException;
import org.openhab.binding.homematic.internal.communicator.client.UnknownRpcFailureException;
Expand All @@ -41,6 +43,7 @@
import org.openhab.binding.homematic.internal.model.TclScript;
import org.openhab.binding.homematic.internal.model.TclScriptDataList;
import org.openhab.binding.homematic.internal.model.TclScriptList;
import org.openhab.core.i18n.ConfigurationException;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
Expand All @@ -60,10 +63,14 @@ public class CcuGateway extends AbstractHomematicGateway {
private Map<String, String> tclregaScripts;
private XStream xStream = new XStream(new StaxDriver());

private @NonNull AuthenticationHandler authenticationHandler;

protected CcuGateway(String id, HomematicConfig config, HomematicGatewayAdapter gatewayAdapter,
HttpClient httpClient) {
HttpClient httpClient) throws IOException, ConfigurationException {
super(id, config, gatewayAdapter, httpClient);

this.authenticationHandler = new AuthenticationHandler(config);

xStream.allowTypesByWildcard(new String[] { HmDevice.class.getPackageName() + ".**" });
xStream.setClassLoader(CcuGateway.class.getClassLoader());
xStream.autodetectAnnotations(true);
Expand Down Expand Up @@ -211,9 +218,9 @@ private synchronized <T> T sendScript(String script, Class<T> clazz) throws IOEx
}

StringContentProvider content = new StringContentProvider(script, config.getEncoding());
ContentResponse response = httpClient.POST(config.getTclRegaUrl()).content(content)
.timeout(config.getTimeout(), TimeUnit.SECONDS)
.header(HttpHeader.CONTENT_TYPE, "text/plain;charset=" + config.getEncoding()).send();
ContentResponse response = authenticationHandler.updateAuthenticationInformation(httpClient
.POST(config.getTclRegaUrl()).content(content).timeout(config.getTimeout(), TimeUnit.SECONDS)
.header(HttpHeader.CONTENT_TYPE, "text/plain;charset=" + config.getEncoding())).send();

String result = new String(response.getContent(), config.getEncoding());
int lastPos = result.lastIndexOf("<xml><exec>");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.client.RpcClient;
import org.openhab.binding.homematic.internal.communicator.client.XmlRpcClient;
import org.openhab.core.i18n.ConfigurationException;

/**
* Factory which evaluates the type of the Homematic gateway and instantiates the appropriate class.
Expand All @@ -30,7 +31,7 @@ public class HomematicGatewayFactory {
* Creates the HomematicGateway.
*/
public static HomematicGateway createGateway(String id, HomematicConfig config,
HomematicGatewayAdapter gatewayAdapter, HttpClient httpClient) throws IOException {
HomematicGatewayAdapter gatewayAdapter, HttpClient httpClient) throws IOException, ConfigurationException {
loadGatewayInfo(config, id, httpClient);
if (config.getGatewayInfo().isCCU()) {
return new CcuGateway(id, config, gatewayAdapter, httpClient);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
Expand All @@ -26,11 +28,14 @@
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.homematic.internal.common.AuthenticationHandler;
import org.openhab.binding.homematic.internal.common.HomematicConfig;
import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
import org.openhab.binding.homematic.internal.communicator.message.XmlRpcRequest;
import org.openhab.binding.homematic.internal.communicator.message.XmlRpcResponse;
import org.openhab.binding.homematic.internal.communicator.parser.RpcResponseParser;
import org.openhab.core.i18n.ConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
Expand All @@ -43,8 +48,9 @@
public class XmlRpcClient extends RpcClient<String> {
private final Logger logger = LoggerFactory.getLogger(XmlRpcClient.class);
private HttpClient httpClient;
private AuthenticationHandler authenticationHandler;

public XmlRpcClient(HomematicConfig config, HttpClient httpClient) throws IOException {
public XmlRpcClient(HomematicConfig config, HttpClient httpClient) throws IOException, ConfigurationException {
super(config);
this.httpClient = httpClient;
}
Expand Down Expand Up @@ -103,11 +109,22 @@ private byte[] send(int port, RpcRequest<String> request) throws IOException {
if (port == config.getGroupPort()) {
url += "/groups";
}
Request req = httpClient.POST(url).content(content).timeout(config.getTimeout(), TimeUnit.SECONDS)
.header(HttpHeader.CONTENT_TYPE, "text/xml;charset=" + config.getEncoding());
if (authenticationHandler == null) {
authenticationHandler = new AuthenticationHandler(config);
}

Request req = authenticationHandler.updateAuthenticationInformation(
httpClient.POST(new URI(url)).content(content).timeout(config.getTimeout(), TimeUnit.SECONDS)
.header(HttpHeader.CONTENT_TYPE, "text/xml;charset=" + config.getEncoding()));

FutureResponseListener listener = new FutureResponseListener(req, config.getBufferSize() * 1024);
req.send(listener);
ContentResponse response = listener.get(config.getTimeout(), TimeUnit.SECONDS);

if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
throw new IOException("Access to Homematic gateway unauthorized");
}

ret = response.getContent();
if (ret == null || ret.length == 0) {
throw new IOException("Received no data from the Homematic gateway");
Expand All @@ -116,7 +133,8 @@ private byte[] send(int port, RpcRequest<String> request) throws IOException {
String result = new String(ret, config.getEncoding());
logger.trace("Client XmlRpcResponse (port {}):\n{}", port, result);
}
} catch (InterruptedException | ExecutionException | TimeoutException | IllegalArgumentException e) {
} catch (InterruptedException | ExecutionException | TimeoutException | IllegalArgumentException
| URISyntaxException | ConfigurationException e) {
throw new IOException(e);
}
return ret;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
import org.openhab.binding.homematic.internal.type.HomematicTypeGenerator;
import org.openhab.binding.homematic.internal.type.UidUtils;
import org.openhab.core.i18n.ConfigurationException;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
Expand Down Expand Up @@ -132,6 +133,9 @@ private void initializeInternal() {
ex.getMessage(), ex);
disposeInternal();
scheduleReinitialize();
} catch (ConfigurationException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
disposeInternal();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ thing-type.config.homematic.bridge.hmIpPort.label = HMIP Port
thing-type.config.homematic.bridge.hmIpPort.description = The port number of the Homematic IP daemon
thing-type.config.homematic.bridge.installModeDuration.label = Install Mode Duration
thing-type.config.homematic.bridge.installModeDuration.description = Time in seconds that the controller will be in install mode when a device discovery is initiated
thing-type.config.homematic.bridge.password.label = Password
thing-type.config.homematic.bridge.password.description = Password for accessing the gateway if authenticaton is required.
thing-type.config.homematic.bridge.rfPort.label = RF Port
thing-type.config.homematic.bridge.rfPort.description = The port number of the RF daemon
thing-type.config.homematic.bridge.socketMaxAlive.label = Socket MaxAlive
Expand All @@ -45,6 +47,10 @@ thing-type.config.homematic.bridge.timeout.label = Timeout
thing-type.config.homematic.bridge.timeout.description = The timeout in seconds for connections to a Homematic gateway
thing-type.config.homematic.bridge.unpairOnDeletion.label = Unpair Devices On Deletion
thing-type.config.homematic.bridge.unpairOnDeletion.description = If set to true, devices are unpaired from the gateway when their corresponding things are removed. The option "factoryResetOnDeletion" also unpairs a device, so in order to avoid unpairing on deletion, both options need to be set to false!
thing-type.config.homematic.bridge.useAuthentication.label = Use authentication
thing-type.config.homematic.bridge.useAuthentication.description = Use authentication to access the gateway. If set to true, username and password is required.
thing-type.config.homematic.bridge.userName.label = User name
thing-type.config.homematic.bridge.userName.description = User name for accessing the gateway if authenticaton is required.
thing-type.config.homematic.bridge.wiredPort.label = Wired Port
thing-type.config.homematic.bridge.wiredPort.description = The port number of the HS485 daemon
thing-type.config.homematic.bridge.xmlCallbackPort.label = XML-RPC Callback Port
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,23 @@
<default>2048</default>
<advanced>true</advanced>
</parameter>
<parameter name="useAuthentication" type="boolean">
<label>Use authentication</label>
<description>Use authentication to access the gateway. If set to true, username and password is required.</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
<parameter name="userName" type="text">
<label>User name</label>
<description>User name for accessing the gateway if authenticaton is required.</description>
<advanced>true</advanced>
</parameter>
<parameter name="password" type="text">
<label>Password</label>
<description>Password for accessing the gateway if authenticaton is required.</description>
<advanced>true</advanced>
<context>password</context>
</parameter>
</config-description>
</bridge-type>

Expand Down