Java tutorial
/** * Copyright (c) 2016, 2017 Red Hat and others * * 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: * Red Hat - initial creation */ package org.eclipse.hono.adapter.mqtt; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.Objects; import io.vertx.core.AsyncResult; import org.apache.qpid.proton.amqp.messaging.Accepted; import org.eclipse.hono.auth.UsernamePasswordCredentials; import org.eclipse.hono.client.MessageSender; import org.eclipse.hono.config.ProtocolAdapterProperties; import org.eclipse.hono.service.AbstractProtocolAdapterBase; import org.eclipse.hono.service.registration.RegistrationAssertionHelperImpl; import org.eclipse.hono.util.Constants; import org.eclipse.hono.util.ResourceIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import io.netty.handler.codec.mqtt.MqttQoS; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.mqtt.MqttEndpoint; import io.vertx.mqtt.MqttServer; import io.vertx.mqtt.MqttServerOptions; import io.vertx.mqtt.messages.MqttPublishMessage; import org.springframework.beans.factory.annotation.Autowired; /** * A Vert.x based Hono protocol adapter for accessing Hono's Telemetry API using MQTT. */ public class VertxBasedMqttProtocolAdapter extends AbstractProtocolAdapterBase<ProtocolAdapterProperties> { private static final Logger LOG = LoggerFactory.getLogger(VertxBasedMqttProtocolAdapter.class); private static final String CONTENT_TYPE_OCTET_STREAM = "application/octet-stream"; private static final String TELEMETRY_ENDPOINT = "telemetry"; private static final String EVENT_ENDPOINT = "event"; private static final int IANA_MQTT_PORT = 1883; private static final int IANA_SECURE_MQTT_PORT = 8883; private MqttServer server; private MqttServer insecureServer; private Map<MqttEndpoint, String> registrationAssertions = new HashMap<>(); private MqttAdapterMetrics metrics; /** * Sets the metrics for this service * * @param metrics The metrics */ @Autowired public final void setMetrics(final MqttAdapterMetrics metrics) { this.metrics = metrics; } @Override public int getPortDefaultValue() { return IANA_SECURE_MQTT_PORT; } @Override public int getInsecurePortDefaultValue() { return IANA_MQTT_PORT; } @Override protected final int getActualPort() { return (server != null ? server.actualPort() : Constants.PORT_UNCONFIGURED); } @Override protected final int getActualInsecurePort() { return (insecureServer != null ? insecureServer.actualPort() : Constants.PORT_UNCONFIGURED); } public void setMqttSecureServer(MqttServer server) { Objects.requireNonNull(server); if (server.actualPort() > 0) { throw new IllegalArgumentException("mqtt server must not be started already"); } else { this.server = server; } } public void setMqttInsecureServer(MqttServer server) { Objects.requireNonNull(server); if (server.actualPort() > 0) { throw new IllegalArgumentException("mqtt server must not be started already"); } else { this.insecureServer = server; } } private Future<MqttServer> bindSecureMqttServer() { if (isSecurePortEnabled()) { MqttServerOptions options = new MqttServerOptions(); options.setHost(getConfig().getBindAddress()).setPort(determineSecurePort()) .setMaxMessageSize(getConfig().getMaxPayloadSize()); addTlsKeyCertOptions(options); addTlsTrustOptions(options); Future<MqttServer> result = Future.future(); result.setHandler(mqttServerAsyncResult -> { server = mqttServerAsyncResult.result(); }); bindMqttServer(options, server, result); return result; } else { return Future.succeededFuture(); } } private Future<MqttServer> bindInsecureMqttServer() { if (isInsecurePortEnabled()) { MqttServerOptions options = new MqttServerOptions(); options.setHost(getConfig().getInsecurePortBindAddress()).setPort(determineInsecurePort()) .setMaxMessageSize(getConfig().getMaxPayloadSize()); Future<MqttServer> result = Future.future(); result.setHandler(mqttServerAsyncResult -> { insecureServer = mqttServerAsyncResult.result(); }); bindMqttServer(options, insecureServer, result); return result; } else { return Future.succeededFuture(); } } private void bindMqttServer(final MqttServerOptions options, final MqttServer mqttServer, final Future<MqttServer> result) { final MqttServer createdMqttServer = (mqttServer == null ? MqttServer.create(this.vertx, options) : mqttServer); createdMqttServer.endpointHandler(this::handleEndpointConnection).listen(done -> { if (done.succeeded()) { LOG.info("Hono MQTT protocol adapter running on {}:{}", getConfig().getBindAddress(), createdMqttServer.actualPort()); result.complete(createdMqttServer); } else { LOG.error("error while starting up Hono MQTT adapter", done.cause()); result.fail(done.cause()); } }); } @Override public void doStart(final Future<Void> startFuture) { LOG.info("limiting size of inbound message payload to {} bytes", getConfig().getMaxPayloadSize()); if (!getConfig().isAuthenticationRequired()) { LOG.warn("authentication of devices switched off"); } checkPortConfiguration().compose(v -> bindSecureMqttServer()).compose(s -> bindInsecureMqttServer()) .compose(insecureServer -> { connectToMessaging(null); connectToDeviceRegistration(null); connectToCredentialsService(null); startFuture.complete(); }, startFuture); } @Override public void doStop(final Future<Void> stopFuture) { Future<Void> shutdownTracker = Future.future(); shutdownTracker.setHandler(done -> { if (done.succeeded()) { LOG.info("MQTT adapter has been shut down successfully"); stopFuture.complete(); } else { LOG.info("error while shutting down MQTT adapter", done.cause()); stopFuture.fail(done.cause()); } }); Future<Void> serverTracker = Future.future(); if (this.server != null) { this.server.close(serverTracker.completer()); } else { serverTracker.complete(); } Future<Void> insecureServerTracker = Future.future(); if (this.insecureServer != null) { this.insecureServer.close(insecureServerTracker.completer()); } else { insecureServerTracker.complete(); } CompositeFuture.all(serverTracker, insecureServerTracker).compose(d -> { closeClients(shutdownTracker.completer()); }, shutdownTracker); } void handleEndpointConnection(final MqttEndpoint endpoint) { LOG.info("connection request from client {}", endpoint.clientIdentifier()); if (!isConnected()) { endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE); } else { if (getConfig().isAuthenticationRequired()) { handleEndpointConnectionWithAuthentication(endpoint); } else { handleEndpointConnectionWithoutAuthentication(endpoint); } } } private void handleEndpointConnectionWithoutAuthentication(final MqttEndpoint endpoint) { endpoint.closeHandler(v -> { LOG.debug("connection closed with client [{}]", endpoint.clientIdentifier()); if (registrationAssertions.remove(endpoint) != null) LOG.trace("removed registration assertion for client [{}]", endpoint.clientIdentifier()); }); endpoint.publishHandler(message -> { final ResourceIdentifier resource = ResourceIdentifier.fromString(message.topicName()); if (resource.getResourceId() == null) { // if MQTT client doesn't specify device_id then closing connection (MQTT has no way for errors) close(endpoint); } if (resource.getTenantId() == null) { // if MQTT client doesn't specify device_id then closing connection (MQTT has no way for errors) close(endpoint); } publishMessage(endpoint, resource.getTenantId(), resource.getResourceId(), message, resource); }); endpoint.accept(false); } private void handleEndpointConnectionWithAuthentication(final MqttEndpoint endpoint) { // check credentials for valid authentication // so far, only hashed-password supported, more to follow if (endpoint.auth() == null) { LOG.trace("no auth information in endpoint found"); endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); return; } UsernamePasswordCredentials authObject = UsernamePasswordCredentials.create(endpoint.auth().userName(), endpoint.auth().password(), getConfig().isSingleTenant()); if (authObject == null) { endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD); return; } validateCredentialsForDevice(authObject.getTenantId(), authObject.getType(), authObject.getAuthId(), authObject.getPassword()) .setHandler(attempt -> handleCredentialsResult(attempt, endpoint, authObject)); } void handleCredentialsResult(final AsyncResult<String> attempt, final MqttEndpoint endpoint, final UsernamePasswordCredentials authObject) { if (attempt.succeeded()) { String logicalDeviceId = attempt.result(); LOG.trace("successfully authenticated device id <{}>", logicalDeviceId); endpoint.accept(false); endpoint.publishHandler(message -> { final ResourceIdentifier resource = ResourceIdentifier.fromString(message.topicName()); if (!validateCredentialsWithTopicStructure(resource, authObject.getTenantId(), logicalDeviceId)) { // if MQTT client does not conform to the topic structure, close the connection (MQTT has no way for errors) endpoint.close(); } else { publishMessage(endpoint, authObject.getTenantId(), logicalDeviceId, message, resource); } }); endpoint.closeHandler(v -> { LOG.debug("connection closed with client [{}], authId [{}], deviceId [{}]", endpoint.clientIdentifier(), authObject.getAuthId(), logicalDeviceId); if (registrationAssertions.remove(endpoint) != null) LOG.trace("removed registration assertion for client [{}]", endpoint.clientIdentifier()); }); } else { endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED); } } private void publishMessage(final MqttEndpoint endpoint, final String tenantId, final String logicalDeviceId, final MqttPublishMessage message, final ResourceIdentifier resource) { LOG.trace("received message [client ID: {}, topic: {}, QoS: {}, payload {}]", endpoint.clientIdentifier(), message.topicName(), message.qosLevel(), message.payload().toString(Charset.defaultCharset())); try { Future<Void> messageTracker = Future.future(); messageTracker.setHandler(s -> { if (s.failed()) { LOG.debug("cannot process message [client ID: {}, deviceId: {}, topic: {}, QoS: {}]: {}", endpoint.clientIdentifier(), logicalDeviceId, resource, message.qosLevel(), s.cause().getMessage()); metrics.incrementUndeliverableMqttMessages(resource.getEndpoint(), tenantId); close(endpoint); } else { LOG.trace("successfully processed message [client ID: {}, deviceId: {}, topic: {}, QoS: {}]", endpoint.clientIdentifier(), logicalDeviceId, resource, message.qosLevel()); metrics.incrementProcessedMqttMessages(resource.getEndpoint(), tenantId); } }); // check that MQTT client tries to publish on topic with device_id same as on connection if (!getConfig().isAuthenticationRequired() && !resource.getResourceId().equals(endpoint.clientIdentifier())) { // MQTT client is trying to publish on a different device_id used on connection (MQTT has no way for // errors) messageTracker.fail("client not authorized"); } else { Future<String> assertionTracker = getRegistrationAssertion(endpoint, tenantId, logicalDeviceId); Future<MessageSender> senderTracker = getSenderTracker(message, resource, tenantId); CompositeFuture.all(assertionTracker, senderTracker).compose(ok -> { doUploadMessage(logicalDeviceId, assertionTracker.result(), endpoint, message, senderTracker.result(), messageTracker); }, messageTracker); } } catch (IllegalArgumentException e) { // MQTT client is trying to publish on invalid topic; it does not contain at least two segments LOG.debug("client [ID: {}] tries to publish on unsupported topic", endpoint.clientIdentifier()); close(endpoint); } } private Future<MessageSender> getSenderTracker(final MqttPublishMessage message, final ResourceIdentifier resource, final String tenantId) { if (resource.getEndpoint().equals(TELEMETRY_ENDPOINT)) { if (!MqttQoS.AT_MOST_ONCE.equals(message.qosLevel())) { // client tries to send telemetry message using QoS 1 or 2 return Future.failedFuture("Only QoS 0 supported for telemetry messages"); } else { return getTelemetrySender(tenantId); } } else if (resource.getEndpoint().equals(EVENT_ENDPOINT)) { if (!MqttQoS.AT_LEAST_ONCE.equals(message.qosLevel())) { // client tries to send event message using QoS 0 or 2 return Future.failedFuture("Only QoS 1 supported for event messages"); } else { return getEventSender(tenantId); } } else { // MQTT client is trying to publish on a not supported endpoint LOG.debug("no such endpoint [{}]", resource.getEndpoint()); return Future.failedFuture("no such endpoint"); } } private Future<String> getRegistrationAssertion(final MqttEndpoint endpoint, final String tenantId, final String logicalDeviceId) { String token = registrationAssertions.get(endpoint); if (token != null && !RegistrationAssertionHelperImpl.isExpired(token, 10)) { return Future.succeededFuture(token); } else { registrationAssertions.remove(endpoint); Future<String> result = Future.future(); getRegistrationAssertion(tenantId, logicalDeviceId).compose(t -> { // if the client closes the connection right after publishing the messages and before that // the registration assertion has been returned, avoid to put it into the map if (endpoint.isConnected()) { LOG.trace("caching registration assertion for client [{}]", endpoint.clientIdentifier()); registrationAssertions.put(endpoint, t); } result.complete(t); }, result); return result; } } @Override protected Future<String> validateCredentialsForDevice(final String tenantId, final String type, final String authId, final Object authenticationObject) { return approveCredentialsAndResolveLogicalDeviceId(tenantId, type, authId, authenticationObject); } private void close(final MqttEndpoint endpoint) { registrationAssertions.remove(endpoint); if (endpoint.isConnected()) { LOG.debug("closing connection with client [client ID: {}]", endpoint.clientIdentifier()); endpoint.close(); } else { LOG.debug("client has already closed connection"); } } void doUploadMessage(final String logicalDeviceId, final String registrationAssertion, final MqttEndpoint endpoint, final MqttPublishMessage message, final MessageSender sender, final Future<Void> uploadHandler) { boolean accepted = sender.send(logicalDeviceId, message.payload().getBytes(), CONTENT_TYPE_OCTET_STREAM, registrationAssertion, (messageId, delivery) -> { LOG.trace("delivery state updated [message ID: {}, new remote state: {}]", messageId, delivery.getRemoteState()); if (message.qosLevel() == MqttQoS.AT_MOST_ONCE) { uploadHandler.complete(); } else if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { if (Accepted.class.isInstance(delivery.getRemoteState())) { // check that the remote MQTT client is still connected before sending PUBACK if (endpoint.isConnected()) { endpoint.publishAcknowledge(message.messageId()); } uploadHandler.complete(); } else { uploadHandler.fail("message not accepted by remote"); } } }); if (!accepted) { uploadHandler.fail("no credit available for sending message"); } } }