
org.openremote.manager.gateway.GatewayClientService Maven / Gradle / Ivy
/*
* Copyright 2020, OpenRemote Inc.
*
* See the CONTRIBUTORS.txt file in the distribution for a
* full listing of individual contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.openremote.manager.gateway;
import io.netty.channel.ChannelHandler;
import org.apache.camel.builder.RouteBuilder;
import org.apache.http.client.utils.URIBuilder;
import org.openremote.agent.protocol.io.AbstractNettyIOClient;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.persistence.PersistenceService;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.asset.AssetProcessingService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.event.ClientEventService;
import org.openremote.manager.rules.AssetQueryPredicate;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.web.ManagerWebService;
import org.openremote.model.Constants;
import org.openremote.model.Container;
import org.openremote.model.ContainerService;
import org.openremote.model.PersistenceEvent;
import org.openremote.model.asset.*;
import org.openremote.model.asset.agent.ConnectionStatus;
import org.openremote.model.attribute.AttributeEvent;
import org.openremote.model.attribute.AttributeRef;
import org.openremote.model.auth.OAuthClientCredentialsGrant;
import org.openremote.model.event.shared.EventFilter;
import org.openremote.model.event.shared.EventSubscription;
import org.openremote.model.event.shared.RealmFilter;
import org.openremote.model.event.shared.SharedEvent;
import org.openremote.model.gateway.*;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.query.filter.RealmPredicate;
import org.openremote.model.syslog.SyslogCategory;
import org.openremote.model.util.Pair;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.ValueUtil;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC;
import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType;
import static org.openremote.container.util.MapAccess.getString;
import static org.openremote.model.syslog.SyslogCategory.GATEWAY;
/**
* Handles outbound connections to central managers
*/
public class GatewayClientService extends RouteBuilder implements ContainerService {
public static final int PRIORITY = ManagerWebService.PRIORITY - 300;
private static final Logger LOG = SyslogCategory.getLogger(GATEWAY, GatewayClientService.class.getName());
public static final String CLIENT_EVENT_SESSION_PREFIX = GatewayClientService.class.getSimpleName() + ":";
public static final String OR_GATEWAY_TUNNEL_LOCALHOST_REWRITE = "OR_GATEWAY_TUNNEL_LOCALHOST_REWRITE";
protected AssetStorageService assetStorageService;
protected AssetProcessingService assetProcessingService;
protected PersistenceService persistenceService;
protected ClientEventService clientEventService;
protected TimerService timerService;
protected ManagerIdentityService identityService;
protected final Map connectionRealmMap = new HashMap<>();
protected final Map clientRealmMap = new HashMap<>();
protected GatewayTunnelFactory gatewayTunnelFactory;
protected Map> clientAttributeTimestamps = new ConcurrentHashMap<>();
protected Consumer realmAssetEventConsumer;
protected Consumer realmAttributeEventConsumer;
@Override
public void init(Container container) throws Exception {
assetStorageService = container.getService(AssetStorageService.class);
assetProcessingService = container.getService(AssetProcessingService.class);
persistenceService = container.getService(PersistenceService.class);
clientEventService = container.getService(ClientEventService.class);
timerService = container.getService(TimerService.class);
identityService = container.getService(ManagerIdentityService.class);
String tunnelKeyFile = getString(container.getConfig(), GatewayService.OR_GATEWAY_TUNNEL_SSH_KEY_FILE, null);
String localhostRewrite = getString(container.getConfig(), OR_GATEWAY_TUNNEL_LOCALHOST_REWRITE, null);
if (!TextUtil.isNullOrEmpty(tunnelKeyFile)) {
File f = new File(tunnelKeyFile);
if (f.exists()) {
LOG.info("Gateway tunnelling SSH key file found at: " + f.getAbsolutePath());
if (!TextUtil.isNullOrEmpty(localhostRewrite)) {
LOG.info("Gateway tunnelling localhostRewrite set to: " + localhostRewrite);
}
gatewayTunnelFactory = new JSchGatewayTunnelFactory(f, localhostRewrite);
} else {
LOG.warning("Gateway tunnelling SSH key file does not exist, tunnelling support disabled: " + f.getAbsolutePath());
}
}
container.getService(ManagerWebService.class).addApiSingleton(
new GatewayClientResourceImpl(timerService, identityService, this)
);
container.getService(MessageBrokerService.class).getContext().addRoutes(this);
clientEventService.addSubscriptionAuthorizer((realm, authContext, eventSubscription) -> {
if (!eventSubscription.isEventType(GatewayConnectionStatusEvent.class)) {
return false;
}
if (authContext == null) {
return false;
}
// If not a super user force a filter for the users realm
if (!authContext.isSuperUser()) {
@SuppressWarnings("unchecked")
EventSubscription subscription = (EventSubscription) eventSubscription;
subscription.setFilter(new RealmFilter<>(authContext.getAuthenticatedRealmName()));
}
return true;
});
}
@Override
public void start(Container container) throws Exception {
// Get existing connections
connectionRealmMap.putAll(persistenceService.doReturningTransaction(entityManager ->
entityManager
.createQuery("select gc from GatewayConnection gc", GatewayConnection.class)
.getResultList()).stream().collect(Collectors.toMap(GatewayConnection::getLocalRealm, gc -> gc)));
// Create clients for enabled connections
connectionRealmMap.forEach((realm, connection) -> {
if (!connection.isDisabled()) {
clientRealmMap.put(realm, createGatewayClient(connection));
clientAttributeTimestamps.put(connection.getLocalRealm(), new ConcurrentHashMap<>());
}
});
}
@Override
public void stop(Container container) throws Exception {
clientRealmMap.forEach((realm, client) -> {
if (client != null) {
destroyGatewayClient(connectionRealmMap.get(realm), client);
}
});
clientRealmMap.clear();
connectionRealmMap.clear();
clientAttributeTimestamps.clear();
}
@Override
public void configure() throws Exception {
from(PERSISTENCE_TOPIC)
.routeId("Persistence-GatewayConnection")
.filter(isPersistenceEventForEntityType(GatewayConnection.class))
.process(exchange -> {
@SuppressWarnings("unchecked")
PersistenceEvent persistenceEvent = exchange.getIn().getBody(PersistenceEvent.class);
GatewayConnection connection = persistenceEvent.getEntity();
processConnectionChange(connection, persistenceEvent.getCause());
});
}
synchronized protected void processConnectionChange(GatewayConnection connection, PersistenceEvent.Cause cause) {
LOG.info("Modified gateway client connection '" + cause + "': " + connection);
synchronized (clientRealmMap) {
switch (cause) {
case UPDATE:
GatewayIOClient client = clientRealmMap.remove(connection.getLocalRealm());
clientAttributeTimestamps.remove(connection.getLocalRealm());
if (client != null) {
destroyGatewayClient(connection, client);
}
case CREATE:
connectionRealmMap.put(connection.getLocalRealm(), connection);
if (!connection.isDisabled()) {
clientRealmMap.put(connection.getLocalRealm(), createGatewayClient(connection));
clientAttributeTimestamps.put(connection.getLocalRealm(), new ConcurrentHashMap<>());
}
break;
case DELETE:
connectionRealmMap.remove(connection.getLocalRealm());
clientAttributeTimestamps.remove(connection.getLocalRealm());
client = clientRealmMap.remove(connection.getLocalRealm());
if (client != null) {
destroyGatewayClient(connection, client);
}
break;
}
}
}
protected GatewayIOClient createGatewayClient(GatewayConnection connection) {
if (connection.isDisabled()) {
LOG.info("Disabled gateway client connection so ignoring: " + connection);
return null;
}
LOG.info("Creating gateway IO client: " + connection);
try {
GatewayIOClient client = new GatewayIOClient(
new URIBuilder()
.setScheme(connection.isSecured() ? "wss" : "ws")
.setHost(connection.getHost())
.setPort(connection.getPort() == null ? -1 : connection.getPort())
.setPath("websocket/events")
.setParameter(Constants.REALM_PARAM_NAME, connection.getRealm()).build(),
null,
new OAuthClientCredentialsGrant(
new URIBuilder()
.setScheme(connection.isSecured() ? "https" : "http")
.setHost(connection.getHost())
.setPort(connection.getPort() == null ? -1 : connection.getPort())
.setPath("auth/realms/" + connection.getRealm() + "/protocol/openid-connect/token")
.build().toString(),
connection.getClientId(),
connection.getClientSecret(),
null).setBasicAuthHeader(true)
);
client.setEncoderDecoderProvider(() ->
new ChannelHandler[] {new AbstractNettyIOClient.MessageToMessageDecoder<>(String.class, client)}
);
client.addConnectionStatusConsumer(
connectionStatus -> onGatewayClientConnectionStatusChanged(connection, connectionStatus)
);
client.addMessageConsumer(message -> onCentralManagerMessage(connection, message));
realmAssetEventConsumer = assetEvent ->
sendCentralManagerMessage(connection.getLocalRealm(), messageToString(SharedEvent.MESSAGE_PREFIX, assetEvent));
// Subscribe to Asset> and attribute events of local realm and pass through to connected manager
clientEventService.addSubscription(
AssetEvent.class,
new AssetFilter().setRealm(connection.getLocalRealm()),
realmAssetEventConsumer);
realmAttributeEventConsumer = attributeEvent ->
sendCentralManagerMessage(connection.getLocalRealm(), messageToString(SharedEvent.MESSAGE_PREFIX, attributeEvent));
clientEventService.addSubscription(
AttributeEvent.class,
getOutboundAttribueEventFilter(connection),
realmAttributeEventConsumer);
client.connect();
return client;
} catch (Exception e) {
LOG.log(Level.WARNING, "Creating gateway IO client failed so marking connection as disabled: " + connection, e);
connection.setDisabled(true);
setConnection(connection);
}
return null;
}
protected EventFilter getOutboundAttribueEventFilter(GatewayConnection gatewayConnection) {
// Convert filters to predicates for efficiency
List> predicatesWithFilters;
if (gatewayConnection.getAttributeFilters() != null && !gatewayConnection.getAttributeFilters().isEmpty()) {
predicatesWithFilters = gatewayConnection.getAttributeFilters()
.stream()
.map(filter -> {
AssetQueryPredicate predicate = filter.getMatcher() != null ? new AssetQueryPredicate(timerService, assetStorageService, filter.getMatcher()) : null;
return new Pair<>(predicate, filter);
})
.toList();
} else {
predicatesWithFilters = Collections.emptyList();
}
return ev -> {
if (!gatewayConnection.getLocalRealm().equals(ev.getRealm())) {
return null;
}
// Allow attribute events that came from the central manager to be returned
if (getClass().getSimpleName().equals(ev.getSource())) {
return ev;
}
boolean allowEvent = predicatesWithFilters.stream()
.filter(predicateWithFilter -> {
if (predicateWithFilter.key == null) {
// Match all
return true;
}
return predicateWithFilter.key.test(ev);
})
.findFirst()
.map(predicatesWithFilter -> {
GatewayAttributeFilter filter = predicatesWithFilter.value;
if (filter.isAllow()) {
return true;
}
if (filter.getSkipAlways() != null && filter.getSkipAlways()) {
return false;
}
if (filter.getValueChange() != null && filter.getValueChange()) {
if (!Objects.equals(ev.getValue(), ev.getOldValue())) {
LOG.finest(() -> "Gateway client for '" + gatewayConnection.getLocalRealm() + "' value change has allowed attribute event: " + ev.getRef());
return true;
}
}
if (filter.getDelta() != null) {
if (Number.class.isAssignableFrom(ev.getTypeClass())) {
double delta = filter.getDelta();
double value = ev.getValue(Double.class).orElse(0d);
double oldValue = ev.getOldValue(Double.class).orElse(0d);
if (Math.abs(value - oldValue) > Math.abs(delta)) {
LOG.finest(() -> "Gateway client for '" + gatewayConnection.getLocalRealm() + "' delta setting has allowed attribute event: " + ev.getRef());
return true;
}
}
}
if (filter.getDurationParsed().isPresent()) {
boolean allow = filter.getDurationParsed().map(durationMillis -> {
Map attributeTimestamps = clientAttributeTimestamps.get(gatewayConnection.getLocalRealm());
Long lastSendMillis = attributeTimestamps.get(ev.getRef());
if (lastSendMillis == null || timerService.getCurrentTimeMillis() - lastSendMillis > durationMillis) {
LOG.finest(() -> "Gateway client for '" + gatewayConnection.getLocalRealm() + "' duration setting has allowed attribute event: " + ev.getRef());
attributeTimestamps.put(ev.getRef(), timerService.getCurrentTimeMillis());
return true;
}
LOG.finest(() -> "Gateway client for '" + gatewayConnection.getLocalRealm() + "' duration setting has blocked attribute event: " + ev.getRef());
return false;
}).orElse(true);
return allow;
}
return false;
}).orElse(true);
return allowEvent ? ev : null;
};
}
protected void destroyGatewayClient(GatewayConnection connection, GatewayIOClient client) {
if (client == null) {
return;
}
LOG.info("Destroying gateway IO client: " + connection);
try {
client.disconnect();
client.removeAllConnectionStatusConsumers();
client.removeAllMessageConsumers();
client.setEncoderDecoderProvider(null);
} catch (Exception e) {
LOG.log(Level.WARNING, "An exception occurred whilst trying to disconnect the gateway IO client", e);
}
if (connection != null) {
clientEventService.removeSubscription(realmAttributeEventConsumer);
clientEventService.removeSubscription(realmAssetEventConsumer);
}
}
protected void onGatewayClientConnectionStatusChanged(GatewayConnection connection, ConnectionStatus connectionStatus) {
LOG.info("Connection status change for gateway IO client '" + connectionStatus + "': " + connection);
clientEventService.publishEvent(new GatewayConnectionStatusEvent(timerService.getCurrentTimeMillis(), connection.getLocalRealm(), connectionStatus));
}
protected void onCentralManagerMessage(GatewayConnection connection, String message) {
SharedEvent event = messageFromString(message, SharedEvent.MESSAGE_PREFIX, SharedEvent.class);
if (event != null) {
if (event instanceof GatewayDisconnectEvent) {
if (((GatewayDisconnectEvent)event).getReason() == GatewayDisconnectEvent.Reason.PERMANENT_ERROR) {
LOG.info("Central manager requested disconnect due to permanent error (likely this version of the edge gateway software is not compatible with that manager version)");
destroyGatewayClient(connection, clientRealmMap.get(connection.getLocalRealm()));
clientRealmMap.put(connection.getLocalRealm(), null);
}
} else if (event instanceof GatewayCapabilitiesRequestEvent) {
LOG.fine("Central manager requested specifications / capabilities of the gateway.");
GatewayCapabilitiesResponseEvent responseEvent = new GatewayCapabilitiesResponseEvent(gatewayTunnelFactory != null);
responseEvent.setMessageID(event.getMessageID());
sendCentralManagerMessage(
connection.getLocalRealm(),
messageToString(SharedEvent.MESSAGE_PREFIX, responseEvent)
);
} else if (event instanceof GatewayTunnelStartRequestEvent gatewayTunnelStartRequestEvent) {
LOG.info("Start tunnel request received: " + gatewayTunnelStartRequestEvent);
String error = null;
try {
gatewayTunnelFactory.startTunnel(gatewayTunnelStartRequestEvent);
} catch (Exception e) {
error = e.getMessage();
}
GatewayTunnelStartResponseEvent responseEvent = new GatewayTunnelStartResponseEvent(error);
responseEvent.setMessageID(event.getMessageID());
sendCentralManagerMessage(
connection.getLocalRealm(),
messageToString(SharedEvent.MESSAGE_PREFIX, responseEvent)
);
} else if (event instanceof GatewayTunnelStopRequestEvent stopRequestEvent) {
LOG.info("Stop tunnel request received: " + stopRequestEvent);
String error = null;
try {
gatewayTunnelFactory.stopTunnel(stopRequestEvent.getInfo());
} catch (Exception e) {
error = e.getMessage();
}
GatewayTunnelStopResponseEvent responseEvent = new GatewayTunnelStopResponseEvent(error);
responseEvent.setMessageID(event.getMessageID());
sendCentralManagerMessage(
connection.getLocalRealm(),
messageToString(SharedEvent.MESSAGE_PREFIX, responseEvent)
);
} else if (event instanceof AttributeEvent) {
assetProcessingService.sendAttributeEvent((AttributeEvent)event, getClass().getSimpleName());
} else if (event instanceof AssetEvent assetEvent) {
if (assetEvent.getCause() == AssetEvent.Cause.CREATE || assetEvent.getCause() == AssetEvent.Cause.UPDATE) {
Asset asset = assetEvent.getAsset();
asset.setRealm(connection.getLocalRealm());
LOG.finest("Request from central manager to create/update an asset: Realm=" + connection.getLocalRealm() + ", Asset> ID=" + asset.getId());
try {
asset = assetStorageService.merge(asset, true);
} catch (Exception e) {
LOG.log(Level.INFO, "Request from central manager to create/update an asset failed: Realm=" + connection.getLocalRealm() + ", Asset> ID=" + asset.getId(), e);
}
}
} else if (event instanceof ReadAssetsEvent readAssets) {
AssetQuery query = readAssets.getAssetQuery();
// Force realm to be the one that this client is associated with
query.realm(new RealmPredicate(connection.getLocalRealm()));
List> assets = assetStorageService.findAll(readAssets.getAssetQuery());
AssetsEvent responseEvent = new AssetsEvent(assets);
responseEvent.setMessageID(event.getMessageID());
sendCentralManagerMessage(
connection.getLocalRealm(),
messageToString(SharedEvent.MESSAGE_PREFIX, responseEvent));
}
}
}
protected void sendCentralManagerMessage(String realm, String message) {
GatewayIOClient client;
synchronized (clientRealmMap) {
client = clientRealmMap.get(realm);
}
if (client != null) {
client.sendMessage(message);
}
}
protected String getClientSessionKey(GatewayConnection connection) {
return CLIENT_EVENT_SESSION_PREFIX + connection.getLocalRealm();
}
protected T messageFromString(String message, String prefix, Class clazz) {
message = message.substring(prefix.length());
return ValueUtil.parse(message, clazz).orElse(null);
}
protected String messageToString(String prefix, Object message) {
String str = ValueUtil.asJSON(message).orElse("null");
return prefix + str;
}
/** GATEWAY RESOURCE METHODS */
protected List getConnections() {
return new ArrayList<>(connectionRealmMap.values());
}
public void setConnection(GatewayConnection connection) {
LOG.info("Updating/creating gateway connection: " + connection);
persistenceService.doTransaction(em -> em.merge(connection));
}
public boolean deleteConnections(List realms) {
LOG.info("Deleting gateway connections for the following realm(s): " + Arrays.toString(realms.toArray()));
try {
persistenceService.doTransaction(em -> {
List connections = em
.createQuery("select gc from GatewayConnection gc where gc.localRealm in :realms", GatewayConnection.class)
.setParameter("realms", realms)
.getResultList();
if (connections.size() != realms.size()) {
throw new IllegalArgumentException("Cannot delete one or more requested gateway connections as they don't exist");
}
connections.forEach(em::remove);
});
} catch (Exception e) {
return false;
}
return true;
}
protected ConnectionStatus getConnectionStatus(String realm) {
GatewayConnection connection = connectionRealmMap.get(realm);
if (connection == null) {
return null;
}
if (connection.isDisabled()) {
return ConnectionStatus.DISABLED;
}
GatewayIOClient client = clientRealmMap.get(realm);
return client != null ? client.getConnectionStatus() : null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy