[homematic] Add Authentication (#16196)

* Add Authentication

---------

Signed-off-by: Christian Kittel <ckittel@gmx.de>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Christian Kittel 2024-02-19 01:09:05 +01:00 committed by Ciprian Pascu
parent 94be9fdca7
commit f1bb5d9936
10 changed files with 186 additions and 12 deletions

View File

@ -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
- 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
@ -180,6 +193,15 @@ Due to the factory reset, the device will also be unpaired from the gateway, eve
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:
```java

View File

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

View File

@ -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.
*/
@ -272,6 +276,48 @@ public class HomematicConfig {
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.
*/

View File

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

View File

@ -22,10 +22,12 @@ import java.util.Map;
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;
@ -41,6 +43,7 @@ import org.openhab.binding.homematic.internal.model.HmResult;
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;
@ -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);
@ -211,9 +218,9 @@ public class CcuGateway extends AbstractHomematicGateway {
}
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>");

View File

@ -18,6 +18,7 @@ import org.eclipse.jetty.client.HttpClient;
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.
@ -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);

View File

@ -14,6 +14,8 @@ package org.openhab.binding.homematic.internal.communicator.client;
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;
@ -26,11 +28,14 @@ import org.eclipse.jetty.client.api.Request;
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;
@ -43,8 +48,9 @@ import org.xml.sax.SAXException;
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;
}
@ -103,11 +109,22 @@ public class XmlRpcClient extends RpcClient<String> {
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");
@ -116,7 +133,8 @@ public class XmlRpcClient extends RpcClient<String> {
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;

View File

@ -36,6 +36,7 @@ import org.openhab.binding.homematic.internal.model.HmDevice;
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;
@ -132,6 +133,9 @@ public class HomematicBridgeHandler extends BaseBridgeHandler implements Homemat
ex.getMessage(), ex);
disposeInternal();
scheduleReinitialize();
} catch (ConfigurationException ex) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
disposeInternal();
}
}
}

View File

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

View File

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