Java tutorial
/** * Copyright (c) 2017 Bosch Software Innovations GmbH. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Bosch Software Innovations GmbH - initial creation */ package org.eclipse.hono.service; import java.net.HttpURLConnection; import java.util.Objects; import io.vertx.proton.ProtonConnection; import org.eclipse.hono.client.CredentialsClient; import org.eclipse.hono.client.HonoClient; import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.client.RegistrationClient; import org.eclipse.hono.config.ServiceConfigProperties; import org.eclipse.hono.service.credentials.SecretsValidator; import org.eclipse.hono.service.credentials.CredentialsUtils; import org.eclipse.hono.util.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import io.vertx.core.AsyncResult; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.ext.healthchecks.HealthCheckHandler; import io.vertx.ext.healthchecks.Status; import io.vertx.proton.ProtonClientOptions; /** * A base class for implementing protocol adapters. * <p> * Provides connections to device registration and telemetry and event service endpoints. * * @param <T> The type of configuration properties used by this service. */ public abstract class AbstractProtocolAdapterBase<T extends ServiceConfigProperties> extends AbstractServiceBase<T> { private HonoClient messaging; private HonoClient registration; private HonoClient credentials; /** * Validates an authentication object for a device. * Needs to be implemented by protocol adapters to support the authentication of devices. * The standard implementation using the * <a href="https://www.eclipse.org/hono/api/Credentials-API/">Credentials API</a> is available by calling * the method {@link #approveCredentialsAndResolveLogicalDeviceId(String, String, String, Object)}; * * @param tenantId The tenantId to which the device belongs. * @param type The type of credentials that are to be used for validation. * @param authId The authId of the credentials that are to be used for validation. * @param authenticationObject The authentication object to be validated, e.g. a password, a preshared-key, etc. * * @return Future The future object carrying the logicalDeviceId, if successful. */ protected abstract Future<String> validateCredentialsForDevice(final String tenantId, final String type, final String authId, final Object authenticationObject); /** * Sets the configuration by means of Spring dependency injection. * <p> * Most protocol adapters will support a single transport protocol to communicate with * devices only. For those adapters there will only be a single bean instance available * in the application context of type <em>T</em>. */ @Autowired @Override public void setConfig(final T configuration) { setSpecificConfig(configuration); } /** * Sets the client to use for connecting to the Hono Messaging component. * * @param honoClient The client. * @throws NullPointerException if hono client is {@code null}. */ @Qualifier(Constants.QUALIFIER_MESSAGING) @Autowired public final void setHonoMessagingClient(final HonoClient honoClient) { this.messaging = Objects.requireNonNull(honoClient); } /** * Gets the client used for connecting to the Hono Messaging component. * * @return The client. */ public final HonoClient getHonoMessagingClient() { return messaging; } /** * Sets the client to use for connecting to the Device Registration service. * * @param registrationServiceClient The client. * @throws NullPointerException if the client is {@code null}. */ @Qualifier(RegistrationConstants.REGISTRATION_ENDPOINT) @Autowired public final void setRegistrationServiceClient(final HonoClient registrationServiceClient) { this.registration = Objects.requireNonNull(registrationServiceClient); } /** * Gets the client used for connecting to the Device Registration service. * * @return The client. */ public final HonoClient getRegistrationServiceClient() { return registration; } /** * Sets the client to use for connecting to the Credentials API (which may be offered by the Device Registry component). * If no credentials client is configured, the registration client will be used for accessing the Credentials API. * * @param credentialsServiceClient The client. * @throws NullPointerException if the client is {@code null}. */ @Qualifier(CredentialsConstants.CREDENTIALS_ENDPOINT) @Autowired(required = false) public final void setCredentialsServiceClient(final HonoClient credentialsServiceClient) { this.credentials = Objects.requireNonNull(credentialsServiceClient); } /** * Gets the client used for connecting to the Credentials API. * * @return The client. */ public final HonoClient getCredentialsServiceClient() { if (credentials == null) { return registration; } else { return credentials; } } @Override public final Future<Void> startInternal() { Future<Void> result = Future.future(); if (messaging == null) { result.fail("Hono Messaging client must be set"); } else if (registration == null) { result.fail("Device Registration client must be set"); } else { if (credentials == null) { LOG.info("Credentials client not configured, using Device Registration client instead."); } doStart(result); } return result; } /** * Subclasses should override this method to perform any work required on start-up of this protocol adapter. * <p> * This method is invoked by {@link #start()} as part of the startup process. * * @param startFuture The future to complete once start up is complete. */ protected void doStart(final Future<Void> startFuture) { // should be overridden by subclasses startFuture.complete(); } @Override public final Future<Void> stopInternal() { Future<Void> result = Future.future(); doStop(result); return result; } /** * Subclasses should override this method to perform any work required before shutting down this protocol adapter. * <p> * This method is invoked by {@link #stop()} as part of the shutdown process. * * @param stopFuture The future to complete once shutdown is complete. */ protected void doStop(final Future<Void> stopFuture) { // to be overridden by subclasses stopFuture.complete(); } /** * Connects to the Hono Messaging component using the configured client. * * @param connectHandler The handler to invoke with the outcome of the connection attempt. * If {@code null} and the connection attempt failed, this method * tries to re-connect until a connection is established. */ protected final void connectToMessaging(final Handler<AsyncResult<HonoClient>> connectHandler) { if (messaging == null) { if (connectHandler != null) { connectHandler.handle(Future.failedFuture("Hono Messaging client not set")); } } else if (messaging.isConnected()) { LOG.debug("already connected to Hono Messaging"); if (connectHandler != null) { connectHandler.handle(Future.succeededFuture(messaging)); } } else { messaging.connect(createClientOptions(), connectAttempt -> { if (connectHandler != null) { connectHandler.handle(connectAttempt); } else { LOG.debug("connected to Hono Messaging"); } }, this::onDisconnectMessaging); } } /** * Attempts a reconnect for the Hono Messaging client after {@link Constants#DEFAULT_RECONNECT_INTERVAL_MILLIS} milliseconds. * * @param con The connection that was disonnected. */ private void onDisconnectMessaging(final ProtonConnection con) { vertx.setTimer(Constants.DEFAULT_RECONNECT_INTERVAL_MILLIS, reconnect -> { LOG.info("attempting to reconnect to Hono Messaging"); messaging.connect(createClientOptions(), connectAttempt -> { if (connectAttempt.succeeded()) { LOG.debug("reconnected to Hono Messaging"); } else { LOG.debug("cannot reconnect to Hono Messaging"); } }, this::onDisconnectMessaging); }); } /** * Connects to the Device Registration service using the configured client. * * @param connectHandler The handler to invoke with the outcome of the connection attempt. * If {@code null} and the connection attempt failed, this method * tries to re-connect until a connection is established. */ protected final void connectToDeviceRegistration(final Handler<AsyncResult<HonoClient>> connectHandler) { if (registration == null) { if (connectHandler != null) { connectHandler.handle(Future.failedFuture("Device Registration client not set")); } } else if (registration.isConnected()) { LOG.debug("already connected to Device Registration service"); if (connectHandler != null) { connectHandler.handle(Future.succeededFuture(registration)); } } else { registration.connect(createClientOptions(), connectAttempt -> { if (connectHandler != null) { connectHandler.handle(connectAttempt); } else { LOG.debug("connected to Device Registration service"); } }, this::onDisconnectDeviceRegistry); } } /** * Attempts a reconnect for the Hono Device Registration client after {@link Constants#DEFAULT_RECONNECT_INTERVAL_MILLIS} milliseconds. * * @param con The connection that was disonnected. */ private void onDisconnectDeviceRegistry(final ProtonConnection con) { vertx.setTimer(Constants.DEFAULT_RECONNECT_INTERVAL_MILLIS, reconnect -> { LOG.info("attempting to reconnect to Device Registration service"); registration.connect(createClientOptions(), connectAttempt -> { if (connectAttempt.succeeded()) { LOG.debug("reconnected to Device Registration service"); } else { LOG.debug("cannot reconnect to Device Registration service"); } }, this::onDisconnectDeviceRegistry); }); } /** * Connects to the Credentials service using the configured client. * * @param connectHandler The handler to invoke with the outcome of the connection attempt. * If {@code null} and the connection attempt failed, this method * tries to re-connect until a connection is established. */ protected final void connectToCredentialsService(final Handler<AsyncResult<HonoClient>> connectHandler) { if (credentials == null) { if (connectHandler != null) { if (registration != null) { // give back registration client if credentials client is not configured connectHandler.handle(Future.succeededFuture(registration)); } else { connectHandler.handle(Future .failedFuture("Neither Credentials client nor Device Registration client is set")); } } } else if (credentials.isConnected()) { LOG.debug("already connected to Credentials service"); if (connectHandler != null) { connectHandler.handle(Future.succeededFuture(credentials)); } } else { credentials.connect(createClientOptions(), connectAttempt -> { if (connectHandler != null) { connectHandler.handle(connectAttempt); } else { LOG.debug("connected to Credentials service"); } }, this::onDisconnectCredentialsService); } } /** * Attempts a reconnect for the Hono Credentials client after {@link Constants#DEFAULT_RECONNECT_INTERVAL_MILLIS} milliseconds. * * @param con The connection that was disonnected. */ private void onDisconnectCredentialsService(final ProtonConnection con) { vertx.setTimer(Constants.DEFAULT_RECONNECT_INTERVAL_MILLIS, reconnect -> { LOG.info("attempting to reconnect to Credentials service"); credentials.connect(createClientOptions(), connectAttempt -> { if (connectAttempt.succeeded()) { LOG.debug("reconnected to Credentials service"); } else { LOG.debug("cannot reconnect to Credentials service"); } }, this::onDisconnectCredentialsService); }); } private ProtonClientOptions createClientOptions() { return new ProtonClientOptions().setConnectTimeout(200).setReconnectAttempts(1) .setReconnectInterval(Constants.DEFAULT_RECONNECT_INTERVAL_MILLIS); } /** * Checks if this adapter is connected to both <em>Hono Messaging</em> and the Device Registration service. * * @return {@code true} if this adapter is connected. */ protected final boolean isConnected() { boolean result = messaging != null && messaging.isConnected() && registration != null && registration.isConnected(); if (credentials != null) { result &= credentials.isConnected(); } return result; } /** * Closes the connections to the Hono Messaging component and the Device Registration service. * * @param closeHandler The handler to notify about the result. */ protected final void closeClients(final Handler<AsyncResult<Void>> closeHandler) { Future<Void> messagingTracker = Future.future(); Future<Void> registrationTracker = Future.future(); Future<Void> credentialsTracker = Future.future(); if (messaging == null) { messagingTracker.complete(); } else { messaging.shutdown(messagingTracker.completer()); } if (registration == null) { registrationTracker.complete(); } else { registration.shutdown(registrationTracker.completer()); } if (credentials == null) { credentialsTracker.complete(); } else { credentials.shutdown(credentialsTracker.completer()); } CompositeFuture.all(messagingTracker, registrationTracker, credentialsTracker).setHandler(s -> { if (closeHandler != null) { if (s.succeeded()) { closeHandler.handle(Future.succeededFuture()); } else { closeHandler.handle(Future.failedFuture(s.cause())); } } }); } /** * Gets a client for sending telemetry data for a tenant. * * @param tenantId The tenant to send the telemetry data for. * @return The client. */ protected final Future<MessageSender> getTelemetrySender(final String tenantId) { Future<MessageSender> result = Future.future(); messaging.getOrCreateTelemetrySender(tenantId, result.completer()); return result; } /** * Gets a client for sending events for a tenant. * * @param tenantId The tenant to send the events for. * @return The client. */ protected final Future<MessageSender> getEventSender(final String tenantId) { Future<MessageSender> result = Future.future(); messaging.getOrCreateEventSender(tenantId, result.completer()); return result; } /** * Gets a client for interacting with the Device Registration service. * * @param tenantId The tenant that the client is scoped to. * @return The client. */ protected final Future<RegistrationClient> getRegistrationClient(final String tenantId) { Future<RegistrationClient> result = Future.future(); getRegistrationServiceClient().getOrCreateRegistrationClient(tenantId, result.completer()); return result; } /** * Gets a client for interacting with the Credentials service. * * @param tenantId The tenant that the client is scoped to. * @return The client. */ protected final Future<CredentialsClient> getCredentialsClient(final String tenantId) { Future<CredentialsClient> result = Future.future(); getCredentialsServiceClient().getOrCreateCredentialsClient(tenantId, result.completer()); return result; } /** * Gets a registration status assertion for a device. * * @param tenantId The tenant that the device belongs to. * @param logicalDeviceId The device to get the assertion for. * @return The assertion. */ protected final Future<String> getRegistrationAssertion(final String tenantId, final String logicalDeviceId) { Future<String> result = Future.future(); getRegistrationClient(tenantId).compose(client -> { Future<RegistrationResult> tokenTracker = Future.future(); client.assertRegistration(logicalDeviceId, tokenTracker.completer()); return tokenTracker; }).compose(regResult -> { if (regResult.getStatus() == HttpURLConnection.HTTP_OK) { result.complete(regResult.getPayload().getString(RegistrationConstants.FIELD_ASSERTION)); } else { result.fail("cannot assert device registration status"); } }, result); return result; } /** * Registers a check that succeeds if this component is connected to Hono Messaging, * the Device Registration and the Credentials service. */ @Override public void registerReadinessChecks(final HealthCheckHandler handler) { handler.register("connection-to-services", status -> { if (isConnected()) { status.complete(Status.OK()); } else { status.complete(Status.KO()); } }); } /** * Registers a check that always succeeds. */ @Override public void registerLivenessChecks(final HealthCheckHandler handler) { handler.register("ping", status -> { status.complete(Status.OK()); }); } private Future<CredentialsObject> getCredentialsForDevice(final String tenantId, final String type, final String authId) { Future<CredentialsObject> result = Future.future(); getCredentialsClient(tenantId).compose(client -> { Future<CredentialsResult<CredentialsObject>> credResultFuture = Future.future(); client.get(type, authId, credResultFuture.completer()); return credResultFuture; }).compose(credResult -> { if (credResult.getStatus() == HttpURLConnection.HTTP_OK) { CredentialsObject payload = credResult.getPayload(); result.complete(payload); } else if (credResult.getStatus() == HttpURLConnection.HTTP_NOT_FOUND) { result.fail(String.format("cannot retrieve credentials (not found for type <%s>, authId <%s>)", type, authId)); } else { result.fail("cannot retrieve credentials"); } }, result); return result; } /** * Validates an authentication object against credentials secrets available by the get operation of the * <a href="https://www.eclipse.org/hono/api/Credentials-API/">Credentials API</a>. * <p>The credentials are first retrieved from the credentials service, and then all matching validators are * invoked until one is successful or all failed. The authentication object is validated iff at least * one validator was successful. * * @param tenantId The tenantId to which the device belongs. * @param type The type of credentials that are to be used for validation. * @param authId The authId of the credentials that are to be used for validation. * @param authenticationObject The authentication object to be validated, e.g. a password, a preshared-key, etc. * @return Future The future object carrying the logicalDeviceId that was resolved by the credentials get operation, if successful. */ protected final Future<String> approveCredentialsAndResolveLogicalDeviceId(final String tenantId, final String type, final String authId, final Object authenticationObject) { return getCredentialsForDevice(tenantId, type, authId).compose(payload -> { Future<String> resultDeviceId = Future.future(); SecretsValidator<Object> validator = CredentialsUtils.findAppropriateValidators(type); try { if (validator != null && validator.validate(payload, authenticationObject)) { resultDeviceId.complete(payload.getDeviceId()); } else { resultDeviceId.fail("credentials invalid - not validated"); } } catch (IllegalArgumentException e) { resultDeviceId.fail(String.format("credentials invalid : %s", e.getMessage())); } return resultDeviceId; }); } /** * Validates that the resource identifier for a protocol adapter message does not contradict to the given tenantIds * and deviceIds. It is not considered an error if the resource identifier does not contain segments for tenantId * and/or deviceId. * * @param resource The resource identifier (built from the MQTT topic name). * @param tenantId The tenantId to validate. * @param logicalDeviceId The logicalDeviceId to validate. * * @return True if the validation was successful, false otherwise. */ protected boolean validateCredentialsWithTopicStructure(final ResourceIdentifier resource, final String tenantId, final String logicalDeviceId) { if (resource.getTenantId() != null && !resource.getTenantId().equals(tenantId)) { return false; } if (resource.getResourceId() != null && !resource.getResourceId().equals(logicalDeviceId)) { return false; } return true; } }