All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.iothub.transport.amqps.AmqpsIotHubConnection Maven / Gradle / Ivy

There is a newer version: 1.0.16
Show newest version
/*
 * Copyright (c) Microsoft. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for full license information.
 */

package com.microsoft.azure.iothub.transport.amqps;

import com.microsoft.azure.iothub.DeviceClientConfig;
import com.microsoft.azure.iothub.IotHubMessageResult;
import com.microsoft.azure.iothub.auth.IotHubSasToken;
import com.microsoft.azure.iothub.net.IotHubUri;
import com.microsoft.azure.iothub.transport.State;
import com.microsoft.azure.iothub.transport.TransportUtils;
import org.apache.qpid.proton.Proton;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.messaging.Accepted;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.messaging.Target;
import org.apache.qpid.proton.amqp.transport.DeliveryState;
import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
import org.apache.qpid.proton.engine.*;
import org.apache.qpid.proton.engine.impl.WebSocketImpl;
import org.apache.qpid.proton.message.Message;
import org.apache.qpid.proton.reactor.FlowController;
import org.apache.qpid.proton.reactor.Handshaker;
import org.apache.qpid.proton.reactor.Reactor;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PEMWriter;


import java.io.*;
import java.nio.BufferOverflowException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;



/**
 * An AMQPS IotHub connection between a device and an IoTHub. This class contains functionality for sending/receiving
 * a message, and logic to re-establish the connection with the IoTHub in case it gets lost.
 */
public final class AmqpsIotHubConnection extends BaseHandler
{
    private int maxWaitTimeForOpeningConnection = 30000;
    protected State state;
    private Future reactorFuture;

    private static final String sendTag = "sender";
    private static final String receiveTag = "receiver";

    private static final String sendEndpointFormat = "/devices/%s/messages/events";
    private final String sendEndpoint;
    private static final String receiveEndpointFormat = "/devices/%s/messages/devicebound";
    private final String receiveEndpoint;

    private int linkCredit = -1;
    /** The {@link Delivery} tag. */
    private long nextTag = 0;
    private static final String versionIdentifierKey = "com.microsoft:client-version";
    private static final String webSocketPath = "/$iothub/websocket";
    private static final String webSocketSubProtocol = "AMQPWSB10";
    private static final int amqpPort = 5671;
    private static final int amqpWebSocketPort = 443;
    private String sasToken;

    private Sender sender;
    private Receiver receiver;
    private Connection connection;
    private Session session;

    private String hostName;
    private String userName;

    private final Boolean useWebSockets;
    protected DeviceClientConfig config;

    private List listeners = new ArrayList<>();
    private ExecutorService executorService;

    /**
     * Constructor to set up connection parameters using the {@link DeviceClientConfig}.
     *
     * @param config The {@link DeviceClientConfig} corresponding to the device associated with this {@link com.microsoft.azure.iothub.DeviceClient}.
     * @param useWebSockets Whether the connection should use web sockets or not.
     */
    public AmqpsIotHubConnection(DeviceClientConfig config, Boolean useWebSockets)
    {
        // Codes_SRS_AMQPSIOTHUBCONNECTION_15_001: [The constructor shall throw IllegalArgumentException if
        // any of the parameters of the configuration is null or empty.]
        if(config == null)
        {
            throw new IllegalArgumentException("The DeviceClientConfig cannot be null.");
        }
        if(config.getIotHubHostname() == null || config.getIotHubHostname().length() == 0)
        {
            throw new IllegalArgumentException("hostName cannot be null or empty.");
        }
        if (config.getDeviceId() == null || config.getDeviceId().length() == 0)
        {
            throw new IllegalArgumentException("deviceID cannot be null or empty.");
        }
        if(config.getIotHubName() == null || config.getIotHubName().length() == 0)
        {
            throw new IllegalArgumentException("hubName cannot be null or empty.");
        }
        if(config.getDeviceKey() == null || config.getDeviceKey().length() == 0)
        {
            throw new IllegalArgumentException("deviceKey cannot be null or empty.");
        }

        // Codes_SRS_AMQPSIOTHUBCONNECTION_15_002: [The constructor shall save the configuration into private member variables.]
        this.config = config;

        String deviceId = this.config.getDeviceId();
        String iotHubName = this.config.getIotHubName();

        this.userName = deviceId + "@sas." + iotHubName;

        this.useWebSockets = useWebSockets;
        if (useWebSockets)
        {
            this.hostName = String.format("%s:%d", this.config.getIotHubHostname(), amqpWebSocketPort);
        }
        else
        {
            this.hostName = String.format("%s:%d", this.config.getIotHubHostname(), amqpPort);
        }

        // Codes_SRS_AMQPSIOTHUBCONNECTION_15_003: [The constructor shall initialize the sender and receiver
        // endpoint private member variables using the send/receiveEndpointFormat constants and device id.]
        this.sendEndpoint = String.format(sendEndpointFormat, deviceId);
        this.receiveEndpoint = String.format(receiveEndpointFormat, deviceId);

        // Codes_SRS_AMQPSIOTHUBCONNECTION_15_004: [The constructor shall initialize a new Handshaker
        // (Proton) object to handle communication handshake.]
        // Codes_SRS_AMQPSIOTHUBCONNECTION_15_005: [The constructor shall initialize a new FlowController
        // (Proton) object to handle communication flow.]
        add(new Handshaker());
        add(new FlowController());

        // Codes_SRS_AMQPSIOTHUBCONNECTION_15_006: [The constructor shall set its state to CLOSED.]
        this.state = State.CLOSED;
    }

    /**
     * Opens the {@link AmqpsIotHubConnection}.
     * 

* If the current connection is not open, this method * will create a new {@link IotHubSasToken}. This method will * start the {@link Reactor}, set the connection to open and make it ready for sending. *

* * @throws IOException If the reactor could not be initialized. */ public void open() throws IOException { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_007: [If the AMQPS connection is already open, the function shall do nothing.] if(this.state == State.CLOSED) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_008: [The function shall create a new sasToken valid for the duration // specified in config to be used for the communication with IoTHub.] this.sasToken = new IotHubSasToken( IotHubUri.getResourceUri(this.config.getIotHubHostname(), this.config.getDeviceId()), this.config.getDeviceKey(), System.currentTimeMillis() / 1000L + this.config.getTokenValidSecs() + 1L).toString(); try { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_009: [The function shall trigger the Reactor (Proton) to begin running.] this.reactorFuture = this.startReactorAsync(); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_010: [The function shall wait for the reactor to be ready and for // enough link credit to become available.] this.connectionReady(); } catch(Exception e) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_011: [If any exception is thrown while attempting to trigger // the reactor, the function shall close the connection and throw an IOException.] this.close(); throw new IOException("Error opening Amqp connection: ", e); } } } /** * Closes the {@link AmqpsIotHubConnection}. *

* If the current connection is not closed, this function * will set the current state to closed and invalidate all connection related variables. *

*/ public void close() { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_048 [If the AMQPS connection is already closed, the function shall do nothing.] // Codes_SRS_AMQPSIOTHUBCONNECTION_15_012: [The function shall set the status of the AMQPS connection to CLOSED.] this.state = State.CLOSED; // Codes_SRS_AMQPSIOTHUBCONNECTION_15_013: [The function shall close the AMQPS sender and receiver links, // the AMQPS session and the AMQPS connection.] if (this.sender != null) this.sender.close(); if (this.receiver != null) this.receiver.close(); if (this.session != null) this.session.close(); if (this.connection != null) this.connection.close(); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_014: [The function shall stop the Proton reactor.] if (this.reactorFuture != null) this.reactorFuture.cancel(true); if (this.executorService != null) this.executorService.shutdown(); } /** * Creates a binary message using the given content and messageId. Sends the created message using the sender link. * @param message The message to be sent. * @return An {@link Integer} representing the hash of the message, or -1 if the connection is closed. */ public Integer sendMessage(Message message) { Integer deliveryHash; // Codes_SRS_AMQPSIOTHUBCONNECTION_15_015: [If the state of the connection is CLOSED or there is not enough // credit, the function shall return -1.] if (this.state == State.CLOSED || this.linkCredit <= 0) { deliveryHash = -1; } else { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_016: [The function shall encode the message and copy the contents to the byte buffer.] byte[] msgData = new byte[1024]; int length; while (true) { try { length = message.encode(msgData, 0, msgData.length); break; } catch (BufferOverflowException e) { msgData = new byte[msgData.length * 2]; } } // Codes_SRS_AMQPSIOTHUBCONNECTION_15_017: [The function shall set the delivery tag for the sender.] byte[] tag = String.valueOf(this. nextTag++).getBytes(); Delivery dlv = sender.delivery(tag); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_018: [The function shall attempt to send the message using the sender link.] sender.send(msgData, 0, length); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_019: [The function shall advance the sender link.] sender.advance(); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_020: [The function shall set the delivery hash to the value returned by the sender link.] deliveryHash = dlv.hashCode(); } // Codes_SRS_AMQPSIOTHUBCONNECTION_15_021: [The function shall return the delivery hash.] return deliveryHash; } /** * Sends the message result for the previously received message. * * @param message the message to be acknowledged. * @param result the message result (one of {@link IotHubMessageResult#COMPLETE}, * {@link IotHubMessageResult#ABANDON}, or {@link IotHubMessageResult#REJECT}). */ public Boolean sendMessageResult(AmqpsMessage message, IotHubMessageResult result) { Boolean ackResult = false; // Codes_SRS_AMQPSIOTHUBCONNECTION_15_022: [If the AMQPS Connection is closed, the function shall return false.] if(this.state != State.CLOSED) { try { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_023: [If the message result is COMPLETE, ABANDON, or REJECT, // the function shall acknowledge the last message with acknowledgement type COMPLETE, ABANDON, or REJECT respectively.] switch (result) { case COMPLETE: message.acknowledge(AmqpsMessage.ACK_TYPE.COMPLETE); break; case REJECT: message.acknowledge(AmqpsMessage.ACK_TYPE.REJECT); break; case ABANDON: message.acknowledge(AmqpsMessage.ACK_TYPE.ABANDON); break; default: // should never happen. throw new IllegalStateException("Invalid IoT Hub message result."); } // Codes_SRS_AMQPSIOTHUBCONNECTION_15_024: [The function shall return true after the message was acknowledged.] ackResult = true; } catch (Exception e) { //do nothing, since ackResult is already false } } return ackResult; } /** * Event handler for the connection init event * @param event The Proton Event object. */ @Override public void onConnectionInit(Event event) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_025: [The event handler shall get the Connection (Proton) object from the event handler and set the host name on the connection.] this.connection = event.getConnection(); this.connection.setHostname(this.hostName); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_026: [The event handler shall create a Session (Proton) object from the connection.] this.session = this.connection.session(); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_027: [The event handler shall create a Receiver and Sender (Proton) links and set the protocol tag on them to a predefined constant.] this.receiver = this.session.receiver(receiveTag); this.sender = this.session.sender(sendTag); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_028: [The Receiver and Sender links shall have the properties set to client version identifier.] Map properties = new HashMap<>(); properties.put(Symbol.getSymbol(versionIdentifierKey), TransportUtils.javaDeviceClientIdentifier + TransportUtils.clientVersion); this.receiver.setProperties(properties); this.sender.setProperties(properties); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_029: [The event handler shall open the connection, session, sender and receiver objects.] this.connection.open(); this.session.open(); receiver.open(); sender.open(); } /** * Event handler for the connection bound event. Sets Sasl authentication and proper authentication mode. * @param event The Proton Event object. */ @Override public void onConnectionBound(Event event) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_030: [The event handler shall get the Transport (Proton) object from the event.] Transport transport = event.getConnection().getTransport(); if(transport != null){ if (this.useWebSockets) { WebSocketImpl webSocket = (WebSocketImpl) transport.webSocket(); webSocket.configure(this.hostName, webSocketPath, 0, webSocketSubProtocol, null, null); } // Codes_SRS_AMQPSIOTHUBCONNECTION_15_031: [The event handler shall set the SASL_PLAIN authentication on the transport using the given user name and sas token.] Sasl sasl = transport.sasl(); sasl.plain(this.userName, this.sasToken); SslDomain domain = makeDomain(SslDomain.Mode.CLIENT); transport.ssl(domain); } } /** * Event handler for reactor init event. * @param event Proton Event object */ @Override public void onReactorInit(Event event) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_033: [The event handler shall set the current handler to handle the connection events.] event.getReactor().connection(this); } /** * Event handler for the delivery event. This method handles both sending and receiving a message. * @param event The Proton Event object. */ @Override public void onDelivery(Event event) { if(event.getLink().getName().equals(receiveTag)) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_034: [If this link is the Receiver link, the event handler shall get the Receiver and Delivery (Proton) objects from the event.] Receiver receiveLink = (Receiver) event.getLink(); Delivery delivery = receiveLink.current(); if (delivery.isReadable() && !delivery.isPartial()) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_035: [The event handler shall read the received buffer.] int size = delivery.pending(); byte[] buffer = new byte[size]; int read = receiveLink.recv(buffer, 0, buffer.length); receiveLink.advance(); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_036: [The event handler shall create an AmqpsMessage object from the decoded buffer.] AmqpsMessage msg = new AmqpsMessage(); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_037: [The event handler shall set the AmqpsMessage Deliver (Proton) object.] msg.setDelivery(delivery); msg.decode(buffer, 0, read); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_049: [All the listeners shall be notified that a message was received from the server.] this.messageReceivedFromServer(msg); } } else { //Sender specific section for dispositions it receives if(event.getType() == Event.Type.DELIVERY) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_038: [If this link is the Sender link and the event type is DELIVERY, the event handler shall get the Delivery (Proton) object from the event.] Delivery d = event.getDelivery(); DeliveryState remoteState = d.getRemoteState(); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_039: [The event handler shall note the remote delivery state and use it and the Delivery (Proton) hash code to inform the AmqpsIotHubConnection of the message receipt.] boolean state = remoteState.equals(Accepted.getInstance()); //let any listener know that the message was received by the server for(ServerListener listener : listeners) { listener.messageSent(d.hashCode(), state); } } } } /** * Event handler for the link flow event. Handles sending a single message. * @param event The Proton Event object. */ @Override public void onLinkFlow(Event event) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_040: [The event handler shall save the remaining link credit.] this.linkCredit = event.getLink().getCredit(); } /** * Event handler for the link remote open event. This signifies that the * {@link org.apache.qpid.proton.reactor.Reactor} is ready, so we set the connection to OPEN. * @param event The Proton Event object. */ @Override public void onLinkRemoteOpen(Event event) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_041: [The connection state shall be considered OPEN when the sender link is open remotely.] Link link = event.getLink(); if (link.getName().equals(sendTag)) { this.state = State.OPEN; } } /** * Event handler for the link remote close event. This triggers reconnection attempts until successful. * Both sender and receiver links closing trigger this event, so we only handle one of them, * since the other is redundant. * @param event The Proton Event object. */ @Override public void onLinkRemoteClose(Event event) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_042 [The event handler shall attempt to reconnect to the IoTHub.] if (event.getLink().getName().equals(sendTag)) { reconnect(); } } /** * Event handler for the link init event. Sets the proper target address on the link. * @param event The Proton Event object. */ @Override public void onLinkInit(Event event) { Link link = event.getLink(); if(link.getName().equals(sendTag)) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_043: [If the link is the Sender link, the event handler shall create a new Target (Proton) object using the sender endpoint address member variable.] Target t = new Target(); t.setAddress(this.sendEndpoint); // Codes_SRS_AMQPSIOTHUBCONNECTION_15_044: [If the link is the Sender link, the event handler shall set its target to the created Target (Proton) object.] link.setTarget(t); // Codes_SRS_AMQPSIOTHUBCONNECTION_14_045: [If the link is the Sender link, the event handler shall set the SenderSettleMode to UNSETTLED.] link.setSenderSettleMode(SenderSettleMode.UNSETTLED); } else { // Codes_SRS_AMQPSIOTHUBCONNECTION_14_046: [If the link is the Receiver link, the event handler shall create a new Source (Proton) object using the receiver endpoint address member variable.] Source source = new Source(); source.setAddress(this.receiveEndpoint); // Codes_SRS_AMQPSIOTHUBCONNECTION_14_047: [If the link is the Receiver link, the event handler shall set its source to the created Source (Proton) object.] link.setSource(source); } } /** * Event handler for the transport error event. This triggers reconnection attempts until successful. * @param event The Proton Event object. */ @Override public void onTransportError(Event event) { // Codes_SRS_AMQPSIOTHUBCONNECTION_15_048: [The event handler shall attempt to reconnect to IoTHub.] this.reconnect(); } /** * Asynchronously runs the Proton {@link Reactor} accepting and sending messages. * @throws IOException if there is an issue creating the reactor. */ private Future startReactorAsync() throws IOException { Reactor reactor = Proton.reactor(this); IotHubReactor iotHubReactor = new IotHubReactor(reactor); executorService = Executors.newFixedThreadPool(1); ReactorRunner reactorRunner = new ReactorRunner(iotHubReactor); return executorService.submit(reactorRunner); } /** * Waits for the reactor to be ready and for enough link credit to be available. * @throws InterruptedException If the current thread was interrupted */ private void connectionReady() throws InterruptedException { int waitTime = 0; while(state == State.CLOSED || this.linkCredit == -1) { Thread.sleep(100); waitTime+=100; if (waitTime > maxWaitTimeForOpeningConnection) { throw new InterruptedException("Waited too long for the connection to open."); } } System.out.println("Connection with the server established successfully."); } /** * Subscribe a listener to the list of listeners. * @param listener the listener to be subscribed. */ public void addListener(ServerListener listener) { listeners.add(listener); } /** * Notifies all listeners that the connection was lost and attempts to reconnect to the IoTHub * using an exponential backoff interval. */ private void reconnect() { this.close(); for(ServerListener listener : listeners) { listener.connectionLost(); } int currentReconnectionAttempt = 1; while (this.state == State.CLOSED) { try { this.open(); } catch (IOException e) { try { System.out.println("Lost connection to the server. Reconnection attempt " + currentReconnectionAttempt++ + "..."); Thread.sleep(TransportUtils.generateSleepInterval(currentReconnectionAttempt)); } catch (InterruptedException ex) { // do nothing, reconnection attempts will continue } } } } /** * Notifies all the listeners that a message was received from the server. * @param msg The message received from server. */ private void messageReceivedFromServer(AmqpsMessage msg) { for(ServerListener listener : listeners) { listener.messageReceived(msg); } } /** * Class which runs the reactor. */ private class ReactorRunner implements Callable { private IotHubReactor iotHubReactor; ReactorRunner(IotHubReactor iotHubReactor) { this.iotHubReactor = iotHubReactor; } @Override public Object call() { iotHubReactor.run(); return null; } } /** * Create Proton SslDomain object from Address using the given Ssl mode * @param mode Proton enum value of requested Ssl mode * @return the created Ssl domain */ private SslDomain makeDomain(SslDomain.Mode mode) { SslDomain domain = Proton.sslDomain(); String trustedDB = getPemFormat(this.config.getPathToCertificate()); if (trustedDB != null ) { domain.setTrustedCaDb(trustedDB); } // Codes_SRS_AMQPSIOTHUBCONNECTION_15_032: [The event handler shall set VERIFY_PEER authentication mode on the domain of the Transport.] if (domain.getTrustedCaDb() != null) { domain.setPeerAuthentication(SslDomain.VerifyMode.VERIFY_PEER); } else { throw new IllegalStateException("SSL connection unsecured, could not find certificate"); } domain.init(mode); return domain; } private String getPemFormat(String certPath) { if (!isPemFile(certPath)) certPath = convertToPem(certPath); return certPath; } private boolean isPemFile(String path) { PEMReader pemReader = null; Reader reader = null; try { reader = new FileReader(path); pemReader = new PEMReader(reader, null); for (String line = pemReader.readLine(); line != null; line = pemReader.readLine()) { if (line.contains("-----BEGIN CERTIFICATE-----")) return true; } } catch (IOException e) { throw new IOError(e); } finally { if (pemReader != null) { try { pemReader.close(); } catch (IOException e) { System.out.println("Couldn't close PEM Reader"); } } if (reader != null) { try { reader.close(); } catch (IOException e) { System.out.println("Couldn't close reader"); } } } return false; } private String convertToPem(String derPath) { FileInputStream in = null; FileWriter writer = null; PEMWriter pemWriter = null; try { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); in = new FileInputStream(derPath); Certificate cert = certFactory.generateCertificate(in); writer = new FileWriter(derPath + ".pem"); try { pemWriter = new PEMWriter(writer); pemWriter.writeObject(cert); } catch (IOException e) { throw new IOError(e); } } catch (IOException e) { throw new IOError(e); } catch (CertificateException e) { throw new IOError(e); } finally { if(pemWriter != null) { try { pemWriter.close(); } catch(IOException e) { System.out.println("Couldn't close PEM writer"); } } if(writer != null) { try { writer.close(); } catch(IOException e) { System.out.println("Couldn't close writer"); } } if(in != null) { try { in.close(); } catch(IOException e) { System.out.println("Couldn't close Input Stream"); } } } return derPath + ".pem" ; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy