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

software.amazon.awssdk.iot.AwsIotMqttConnectionBuilder Maven / Gradle / Ivy

There is a newer version: 1.23.0
Show newest version

/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */
package software.amazon.awssdk.iot;

import software.amazon.awssdk.crt.utils.PackageInfo;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;

import software.amazon.awssdk.crt.CRT;
import software.amazon.awssdk.crt.CrtResource;
import software.amazon.awssdk.crt.CrtRuntimeException;
import software.amazon.awssdk.crt.Log;
import software.amazon.awssdk.crt.Log.LogLevel;
import software.amazon.awssdk.crt.Log.LogSubject;
import software.amazon.awssdk.crt.auth.credentials.CredentialsProvider;
import software.amazon.awssdk.crt.auth.credentials.DefaultChainCredentialsProvider;
import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig;
import software.amazon.awssdk.crt.http.HttpProxyOptions;
import software.amazon.awssdk.crt.io.ClientBootstrap;
import software.amazon.awssdk.crt.io.ClientTlsContext;
import software.amazon.awssdk.crt.io.SocketOptions;
import software.amazon.awssdk.crt.io.TlsContextCustomKeyOperationOptions;
import software.amazon.awssdk.crt.io.TlsContextOptions;
import software.amazon.awssdk.crt.io.TlsContextPkcs11Options;
import software.amazon.awssdk.crt.mqtt.MqttClient;
import software.amazon.awssdk.crt.mqtt.MqttClientConnection;
import software.amazon.awssdk.crt.mqtt.MqttClientConnectionEvents;
import software.amazon.awssdk.crt.mqtt.MqttConnectionConfig;
import software.amazon.awssdk.crt.mqtt.MqttException;
import software.amazon.awssdk.crt.mqtt.MqttMessage;
import software.amazon.awssdk.crt.mqtt.QualityOfService;
import software.amazon.awssdk.crt.mqtt.WebsocketHandshakeTransformArgs;

/**
 * A central class for building Mqtt connections without manually managing a large variety of native objects (some
 * still need to be created though).
 */
public final class AwsIotMqttConnectionBuilder extends CrtResource {

    private static String IOT_SIGNING_SERVICE = "iotdevicegateway";
    private static String AMZ_DATE_HEADER = "x-amz-date";
    private static String AMZ_SECURITY_TOKEN_HEADER = "x-amz-security-token";

    MqttConnectionConfig config;

    /* Internal config and state */
    private MqttClient client; // Lazy create, cached
    private CredentialsProvider websocketCredentialsProvider;
    private String websocketSigningRegion;
    private ClientTlsContext tlsContext;  // Lazy create, cached
    private TlsContextOptions tlsOptions;
    private ClientBootstrap bootstrap;

    private boolean resetLazilyCreatedResources = true;
    // Used to detect if we need to set the ALPN list for custom authorizer
    private boolean isUsingCustomAuthorizer = false;

    private void resetDefaultPort() {
        if (TlsContextOptions.isAlpnSupported()) {
            this.tlsOptions.withAlpnList("x-amzn-mqtt-ca");
            this.config.setPort(443);
        } else {
            this.config.setPort(8883);
        }
    }

    private AwsIotMqttConnectionBuilder(TlsContextOptions tlsOptions) {
        try (MqttConnectionConfig connectionConfig = new MqttConnectionConfig()) {
            addReferenceTo(connectionConfig);
            config = connectionConfig;
        }

        this.tlsOptions = tlsOptions;
        addReferenceTo(tlsOptions);

        resetDefaultPort();
    }

    /**
     * Required override method that must begin the release process of the acquired native handle
     */
    @Override
    protected void releaseNativeHandle() {}

    /**
     * Override that determines whether a resource releases its dependencies at the same time the native handle is released or if it waits.
     * Resources with asynchronous shutdown processes should override this with false, and establish a callback from native code that
     * invokes releaseReferences() when the asynchronous shutdown process has completed.  See HttpClientConnectionManager for an example.
     */
    @Override
    protected boolean canReleaseReferencesImmediately() { return true; }


    /**
     * Create a new builder with mTLS file paths
     *
     * @param certPath       - Path to certificate, in PEM format
     * @param privateKeyPath - Path to private key, in PEM format
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public static AwsIotMqttConnectionBuilder newMtlsBuilderFromPath(String certPath, String privateKeyPath) {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions.createWithMtlsFromPath(certPath, privateKeyPath)) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Create a new builder with mTLS cert pair in memory
     *
     * @param certificate - Certificate, in PEM format
     * @param privateKey  - Private key, in PEM format
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public static AwsIotMqttConnectionBuilder newMtlsBuilder(String certificate, String privateKey) {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions.createWithMtls(certificate, privateKey)) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Create a new builder with mTLS cert pair in memory
     *
     * @param certificate - Certificate, in PEM format
     * @param privateKey  - Private key, in PEM format
     * @return {@link AwsIotMqttConnectionBuilder}
     * @throws UnsupportedEncodingException if encoding is unsupported
     */
    public static AwsIotMqttConnectionBuilder newMtlsBuilder(byte[] certificate, byte[] privateKey)
            throws UnsupportedEncodingException {
        return newMtlsBuilder(new String(certificate, "UTF8"), new String(privateKey, "UTF8"));
    }

    /**
     * Create a new builder with mTLS, using a PKCS#11 library for private key operations.
     *
     * NOTE: Unix only
     *
     * @param pkcs11Options PKCS#11 options
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public static AwsIotMqttConnectionBuilder newMtlsPkcs11Builder(TlsContextPkcs11Options pkcs11Options) {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions.createWithMtlsPkcs11(pkcs11Options)) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Create a new builder with mTLS, using a custom handler for private key operations.
     *
     * @param operationOptions options for using a custom handler
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public static AwsIotMqttConnectionBuilder newMtlsCustomKeyOperationsBuilder(TlsContextCustomKeyOperationOptions operationOptions) {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions.createWithMtlsCustomKeyOperations(operationOptions)) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Create a new builder with mTLS, using a certificate in a Windows certificate store.
     *
     * NOTE: Windows only
     *
     * @param certificatePath Path to certificate in a Windows certificate store.
     *                        The path must use backslashes and end with the
     *                        certificate's thumbprint. Example:
     *                        {@code CurrentUser\MY\A11F8A9B5DF5B98BA3508FBCA575D09570E0D2C6}
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public static AwsIotMqttConnectionBuilder newMtlsWindowsCertStorePathBuilder(String certificatePath) {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions
                .createWithMtlsWindowsCertStorePath(certificatePath)) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Create a new builder with mTLS, using a certificate and key stored in the passed-in Java keystore.
     *
     * Note: function assumes the passed keystore has already been loaded from a file by calling "keystore.load(file, password)"
     *
     * @param keyStore The Java keystore to use. Assumed to be loaded with certificates and keys
     * @param certificateAlias The alias of the certificate and key to use with the builder.
     * @param certificatePassword The password of the certificate and key to use with the builder.
     * @throws CrtRuntimeException if an error occurs, like the keystore cannot be opened or the certificate is not found.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public static AwsIotMqttConnectionBuilder newJavaKeystoreBuilder(
        java.security.KeyStore keyStore, String certificateAlias, String certificatePassword) throws CrtRuntimeException {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions
                .createWithMtlsJavaKeystore(keyStore, certificateAlias, certificatePassword)) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Create a new builder with mTLS, using a PKCS12 library for private key operations.
     *
     * NOTE: MacOS only
     *
     * @param pkcs12Path Path to the PKCS12 file to use with the builder.
     * @param pkcs12Password The password of the PKCS12 file to use with the builder.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public static AwsIotMqttConnectionBuilder newMtlsPkcs12Builder(
            String pkcs12Path, String pkcs12Password) {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions.createWithMtlsPkcs12(pkcs12Path, pkcs12Password)) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Create a new builder with no default Tls options
     *
     * @return a new builder with default Tls options
     * @throws UnsupportedEncodingException if encoding is unsupported
     */
    public static AwsIotMqttConnectionBuilder newDefaultBuilder()
            throws UnsupportedEncodingException {
        try (TlsContextOptions tlsContextOptions = TlsContextOptions.createDefaultClient()) {
            return new AwsIotMqttConnectionBuilder(tlsContextOptions);
        }
    }

    /**
     * Overrides the default system trust store.
     *
     * @param caDirPath  - Only used on Unix-style systems where all trust anchors
     *                    are stored in a directory (e.g. /etc/ssl/certs).
     * @param caFilePath - Single file containing all trust CAs, in PEM format
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withCertificateAuthorityFromPath(String caDirPath, String caFilePath) {
        this.tlsOptions.overrideDefaultTrustStoreFromPath(caDirPath, caFilePath);
        resetLazilyCreatedResources = true;
        return this;
    }

    /**
     * Overrides the default system trust store.
     *
     * @param caRoot - Buffer containing all trust CAs, in PEM format
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withCertificateAuthority(String caRoot) {
        this.tlsOptions.overrideDefaultTrustStore(caRoot);
        resetLazilyCreatedResources = true;
        return this;
    }

    /**
     * Configures the IoT endpoint for connections from this builder.
     *
     * @param endpoint The IoT endpoint to connect to
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withEndpoint(String endpoint) {
        this.config.setEndpoint(endpoint);
        return this;
    }

    /**
     * Configures the port to connect to for connections from this builder.  If not set, 443 will be used for
     * a websocket connection or where ALPN support is available.  Otherwise the default is 8883.
     *
     * @param port The port to connect to on the IoT endpoint. Usually 8883 for
     *             MQTT, or 443 for websockets
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withPort(int port) {
        this.config.setPort(port);
        return this;
    }

    /**
     * Configures the client id to use to connect to the IoT Core service.
     *
     * @param clientId The client id for connections from this builder. Needs to be unique across
     *                  all devices/clients.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withClientId(String clientId) {
        this.config.setClientId(clientId);
        return this;
    }

    /**
     * Configures whether or not the service should try to resume prior
     * subscriptions, if it has any
     *
     * @param cleanSession true if the session should drop prior subscriptions when
     *                     a connection from this builder is established, false to resume the session
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withCleanSession(boolean cleanSession) {
        this.config.setCleanSession(cleanSession);
        return this;
    }

    /**
     * @deprecated Configures MQTT keep-alive via PING messages. Note that this is not TCP
     * keepalive.
     *
     * @param keepAliveMs How often in milliseconds to send an MQTT PING message to the
     *                   service to keep connections alive
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    @Deprecated
    public AwsIotMqttConnectionBuilder withKeepAliveMs(int keepAliveMs) {
        this.config.setKeepAliveSecs(keepAliveMs/1000);
        return this;
    }

    /**
     * Configures MQTT keep-alive via PING messages. Note that this is not TCP
     * keepalive.
     *
     * @param keepAliveSecs How often in seconds to send an MQTT PING message to the
     *                   service to keep connections alive
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withKeepAliveSecs(int keepAliveSecs) {
        this.config.setKeepAliveSecs(keepAliveSecs);
        return this;
    }

    /**
     * Controls ping timeout value.  If a response is not received within this
     * interval, the connection will be reestablished.
     *
     * @param pingTimeoutMs How long to wait for a ping response before resetting a connection built from this
     *                        builder.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withPingTimeoutMs(int pingTimeoutMs) {
        this.config.setPingTimeoutMs(pingTimeoutMs);
        return this;
    }

    /**
     * Controls timeout value for requests that response is required on healthy connection.
     * If a response is not received within this interval, the request will fail as server not receiving it.
     * Applied to publish (QoS greater than 0) and unsubscribe
     *
     * @param protocolOperationTimeoutMs How long to wait for a request response (in milliseconds) before failing
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withProtocolOperationTimeoutMs(int protocolOperationTimeoutMs) {
        this.config.setProtocolOperationTimeoutMs(protocolOperationTimeoutMs);
        return this;
    }

    /**
     * Configures the TCP socket connect timeout (in milliseconds)
     *
     * @param timeoutMs TCP socket timeout
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withTimeoutMs(int timeoutMs) {
        if (this.config.getSocketOptions() == null) {
            this.withSocketOptions(new SocketOptions());
        }
        this.config.getSocketOptions().connectTimeoutMs = timeoutMs;
        return this;
    }

    /**
     * Configures the minimum and maximum reconnect timeouts.
     *
     * The time between reconnect attempts will start at min and multiply by 2 until max is reached.
     *
     * @param minTimeoutSecs The timeout to start with
     * @param maxTimeoutSecs The highest allowable wait time between reconnect attempts
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withReconnectTimeoutSecs(long minTimeoutSecs, long maxTimeoutSecs) {
        this.config.setReconnectTimeoutSecs(minTimeoutSecs, maxTimeoutSecs);
        return this;
    }

    /**
     * Configures the common settings for the socket to use for connections created by this builder
     *
     * @param socketOptions The socket settings
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withSocketOptions(SocketOptions socketOptions) {
        this.config.setSocketOptions(socketOptions);
        return this;
    }

    /**
     * Configures the username to include in the initial CONNECT mqtt packet for connections created by this builder.
     *
     * @param username username to use in CONNECT
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withUsername(String username) {
        this.config.setUsername(username);
        return this;
    }

    /**
     * Configures the password to include in the initial CONNECT mqtt packet for connections created by this builder.
     *
     * @param password password to use in CONNECT
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withPassword(String password) {
        this.config.setPassword(password);
        return this;
    }

    /**
     * Configures the connection-related callbacks to use for connections created by this builder
     *
     * @param callbacks connection event callbacks to use
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withConnectionEventCallbacks(MqttClientConnectionEvents callbacks) {
        this.config.setConnectionCallbacks(callbacks);
        return this;
    }

    /**
     * Sets the last will and testament message to be delivered to a topic when a connection created by this builder
     * disconnects
     *
     * @param message The message to publish as the will. The message contains the
     *                topic that the message will be published to on disconnect,
     *                along with the {@link QualityOfService} that it will be
     *                published with and whether it will be retained when it is published.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withWill(MqttMessage message) throws MqttException {
        this.config.setWillMessage(message);

        return this;
    }

    /**
     * @deprecated Use alternate withWill(). QoS and retain are now set directly on the {@link MqttMessage}
     * @param message deprecated
     * @param qos deprecated
     * @param retain depricated
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    @Deprecated
    public AwsIotMqttConnectionBuilder withWill(MqttMessage message, QualityOfService qos, boolean retain) throws MqttException {
        return withWill(new MqttMessage(message.getTopic(), message.getPayload(), qos, retain));
    }

    /**
     * Configures the client bootstrap to use for connections created by this builder
     *
     * @param bootstrap client bootstrap to use for created connections
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withBootstrap(ClientBootstrap bootstrap) {
        swapReferenceTo(this.bootstrap, bootstrap);
        this.bootstrap = bootstrap;
        resetLazilyCreatedResources = true;

        return this;
    }

    /**
     * Configures whether or not to the connection uses websockets
     *
     * @param useWebsockets whether or not to use websockets
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withWebsockets(boolean useWebsockets) {
        this.config.setUseWebsockets(useWebsockets);

        if (useWebsockets) {
            this.tlsOptions.alpnList.clear();
            this.config.setPort(443);
        } else {
            resetDefaultPort();
        }

        resetLazilyCreatedResources = true;

        return this;
    }

    /**
     * Configures handshake transform used when establishing a connection via websockets.  If no transform has been
     * set then a default transform is used that adds AWS IoT authentication parameters and signs the request via
     * Sigv4.
     *
     * When done mutating the request, complete MUST be called on the future contained within the
     * transform args parameter.
     *
     * @param handshakeTransform handshake request transformation function
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withWebsocketHandshakeTransform(Consumer handshakeTransform) {
        this.config.setWebsocketHandshakeTransform(handshakeTransform);

        return this;
    }

    /**
     * @deprecated use withHttpProxyOptions instead
     * Configures any http proxy options to use if the connection uses websockets
     *
     * @param proxyOptions http proxy options to use when establishing a websockets-based connection
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withWebsocketProxyOptions(HttpProxyOptions proxyOptions) {
        this.config.setHttpProxyOptions(proxyOptions);

        return this;
    }

    /**
     * Configures any http proxy options to use
     *
     * @param proxyOptions http proxy options to use when establishing a connection
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withHttpProxyOptions(HttpProxyOptions proxyOptions) {
        this.config.setHttpProxyOptions(proxyOptions);

        return this;
    }

    /**
     * Configures the region to use when signing (via Sigv4) the websocket upgrade request.  Only applicable
     * if the handshake transform is null (enabling the default sigv4 transform injection).
     *
     * @param region region to use when signing the websocket upgrade request
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withWebsocketSigningRegion(String region) {
        this.websocketSigningRegion = region;

        return this;
    }

    /**
     * Configures the credentials provider to use for websocket handshake signing.  Only applicable to sigv4
     * based authentication.  If provider is null, the default provider chain will be used.
     *
     * @param provider  credentials provider to pull Aws credentials from.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withWebsocketCredentialsProvider(CredentialsProvider provider) {
        swapReferenceTo(this.websocketCredentialsProvider, provider);
        this.websocketCredentialsProvider = provider;

        return this;
    }

    /**
     * A helper function to add parameters to the username in the withCustomAuthorizer function
     */
    private String addUsernameParameter(String inputString, String parameterValue, String parameterPreText, Boolean addedStringToUsername) {
        String return_string = inputString;
        if (addedStringToUsername == false) {
            return_string += "?";
        } else {
            return_string += "&";
        }

        if (parameterValue.contains(parameterPreText)) {
            return return_string + parameterValue;
        } else {
            return return_string + parameterPreText + parameterValue;
        }
    }

    /**
     * Configures the MQTT connection so it can use a custom authorizer.
     * This function will modify the username, port, and TLS options.
     *
     * @deprecated Please use the full withCustomAuthorizer function that includes `tokenKeyName` and
     *             `tokenValue`. This version is left for backwards compatibility purposes.
     *
     * Note: All arguments are optional and can have "null" as valid input.
     * See the description for each argument for information on what happens if null is passed.
     * @param username The username to use with the custom authorizer. If null is passed, it will check to
     *                 see if a username has already been set (via withUsername function). If no username is set then
     *                 no username will be passed with the MQTT connection.
     * @param authorizerName The name of the custom authorizer. If null is passed, then 'x-amz-customauthorizer-name'
     *                       will not be added with the MQTT connection.
     * @param authorizerSignature The signature of the custom authorizer.
     *                            NOTE: This will NOT work without the token key name and token value, which requires
     *                            using the non-depreciated API.
     * @param password The password to use with the custom authorizer. If null is passed, then no password will be set.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withCustomAuthorizer(String username, String authorizerName, String authorizerSignature, String password) {
        return this.withCustomAuthorizer(username, authorizerName, authorizerSignature, password, null, null);
    }

    /**
     * Configures the MQTT connection so it can use a custom authorizer.
     * This function will modify the username, port, and TLS options.
     *
     * Note: All arguments are optional and can have "null" as valid input.
     * See the description for each argument for information on what happens if null is passed.
     * @param username The username to use with the custom authorizer. If null is passed, it will check to
     *                 see if a username has already been set (via withUsername function). If no username is set then
     *                 no username will be passed with the MQTT connection.
     * @param authorizerName The name of the custom authorizer. If null is passed, then 'x-amz-customauthorizer-name'
     *                       will not be added with the MQTT connection.
     * @param authorizerSignature The signature of the custom authorizer. If null is passed, then
     *                            'x-amz-customauthorizer-signature' will not be added with the MQTT connection.
     *                            The signature must be based on the private key associated with the custom authorizer.
     *                            The signature must be base64 encoded. Required if the custom authorizer has signing
     *                            enabled. It is strongly suggested to URL-encode this value; the SDK will not do
     *                            so for you.
     * @param password The password to use with the custom authorizer. If null is passed, then no password will be set.
     * @param tokenKeyName Key used to extract the custom authorizer token from MQTT username query-string properties.
     *                     Required if the custom authorizer has signing enabled.  It is strongly suggested to URL-encode
     *                     this value; the SDK will not do so for you.
     * @param tokenValue An opaque token value.
     *                   Required if the custom authorizer has signing enabled. This value must be signed by the private
     *                   key associated with the custom authorizer and the result placed in the authorizerSignature argument.
     * @return {@link AwsIotMqttConnectionBuilder}
     */
    public AwsIotMqttConnectionBuilder withCustomAuthorizer(String username, String authorizerName, String authorizerSignature, String password, String tokenKeyName, String tokenValue) {
        if (authorizerSignature != null || tokenKeyName != null || tokenValue != null) {
            if (tokenKeyName == null || tokenValue == null || authorizerSignature == null) {
                throw new RuntimeException("Token-based custom authentication requires all token-related properties to be set");
            }
        }

        isUsingCustomAuthorizer = true;
        String usernameString = "";
        Boolean addedStringToUsername = false;

        if (username == null) {
            if (config.getUsername() != null) {
                usernameString += config.getUsername();
            }
        } else {
            usernameString += username;
        }

        if (authorizerName != null) {
            usernameString = addUsernameParameter(usernameString, authorizerName, "x-amz-customauthorizer-name=", addedStringToUsername);
            addedStringToUsername = true;
        }

        if (authorizerSignature != null)
        {
            if (!authorizerSignature.contains("%")) {
                try {
                    authorizerSignature = URLEncoder.encode(authorizerSignature, StandardCharsets.UTF_8.toString());
                } catch (UnsupportedEncodingException uee) {
                    throw new CrtRuntimeException(uee.toString());
                }
            }

            usernameString = addUsernameParameter(usernameString, authorizerSignature, "x-amz-customauthorizer-signature=", addedStringToUsername);
        }

        if (tokenKeyName != null && tokenValue != null) {
            usernameString = addUsernameParameter(usernameString, tokenValue, tokenKeyName + "=", addedStringToUsername);
        }

        config.setUsername(usernameString);

        if (password != null) {
            config.setPassword(password);
        }

        if (config.getUseWebsockets() == false) {
            tlsOptions.alpnList.clear();
            tlsOptions.alpnList.add("mqtt");
        }
        config.setPort(443);

        return this;
    }

    /**
     * Converts a AwsIotMqttConnectionBuilder to a AwsIotMqtt5ClientBuilder, converting as much as possible.
     * See AwsIotMqtt5ClientBuilder.newMqttBuilderFromMqtt311ConnectionConfig for more information on what can
     * be converted, the quirks, etc.
     *
     * @return A AwsIotMqtt5ClientBuilder using AwsIotMqttConnectionBuilder settings
     * @throws Exception If an unsupported option is present
     */
    public AwsIotMqtt5ClientBuilder toAwsIotMqtt5ClientBuilder() throws Exception {
        return AwsIotMqtt5ClientBuilder.newMqttBuilderFromMqtt311ConnectionConfig(config, tlsOptions);
    }

    /**
     * Builds a new mqtt connection from the configuration stored in the builder.  Because some objects are created
     * lazily, certain properties should not be modified after this is first invoked (tls options, bootstrap).
     *
     * @return a new mqtt connection
     */
    public MqttClientConnection build() {
        // Validate
        if (bootstrap == null) {
            bootstrap = ClientBootstrap.getOrCreateStaticDefault();
        }

        // Lazy create
        // This does mean that once you call build() once, modifying the tls context options or client bootstrap
        // has no affect on subsequently-created connections.
        synchronized(this) {

            // Check to see if a custom authorizer is being used but not through the builder.
            if (isUsingCustomAuthorizer == false) {
                if (config.getUsername() != null) {
                    if (config.getUsername().contains("x-amz-customauthorizer-name=") ||
                        config.getUsername().contains("x-amz-customauthorizer-signature="))
                    {
                        isUsingCustomAuthorizer = true;
                    }
                }
            }
            // Is the user trying to connect using a custom authorizer?
            if (isUsingCustomAuthorizer == true) {
                if (config.getPort() != 443) {
                    Log.log(LogLevel.Warn, LogSubject.MqttClient,"Attempting to connect to authorizer with unsupported port. Port is not 443...");
                }
                if (config.getUseWebsockets() == false) {
                    if (tlsOptions.alpnList.size() == 1) {
                        if (tlsOptions.alpnList.get(0) != "mqtt") {
                            tlsOptions.alpnList.clear();
                            tlsOptions.alpnList.add("mqtt");
                        }
                    } else {
                        tlsOptions.alpnList.clear();
                        tlsOptions.alpnList.add("mqtt");
                    }
                }
            }

            if (tlsOptions != null && (tlsContext == null || resetLazilyCreatedResources)) {
                try (ClientTlsContext clientTlsContext = new ClientTlsContext(tlsOptions)) {
                    swapReferenceTo(tlsContext, clientTlsContext);
                    tlsContext = clientTlsContext;
                }
            }

            if (client == null || resetLazilyCreatedResources) {
                try (MqttClient mqttClient = (tlsContext == null) ? new MqttClient(bootstrap) : new MqttClient(bootstrap, tlsContext)) {
                    swapReferenceTo(client, mqttClient);
                    client = mqttClient;
                    config.setMqttClient(client);
                }
            }
        }

        resetLazilyCreatedResources = false;

        // Connection create
        try (MqttConnectionConfig connectionConfig = config.clone()) {

            // Whether or not a username has been added, append our metrics tokens
            String usernameOrEmpty = "";
            if (connectionConfig.getUsername() != null) {
                usernameOrEmpty = connectionConfig.getUsername();
            }
            String queryStringConcatenation = "?";
            if (usernameOrEmpty.contains("?")) {
                queryStringConcatenation = "&";
            }

            if(CRT.getOSIdentifier() == "android"){
                connectionConfig.setUsername(String.format("%s%sSDK=AndroidV2&Version=%s",
                usernameOrEmpty,
                queryStringConcatenation,
                new PackageInfo().version.toString()));
            } else {
                connectionConfig.setUsername(String.format("%s%sSDK=JavaV2&Version=%s",
                usernameOrEmpty,
                queryStringConcatenation,
                new PackageInfo().version.toString()));
            }

            if (connectionConfig.getUseWebsockets() && connectionConfig.getWebsocketHandshakeTransform() == null) {
                if (websocketCredentialsProvider == null) {
                    DefaultChainCredentialsProvider.DefaultChainCredentialsProviderBuilder providerBuilder = new DefaultChainCredentialsProvider.DefaultChainCredentialsProviderBuilder();
                    providerBuilder.withClientBootstrap(bootstrap);

                    try (CredentialsProvider defaultProvider = providerBuilder.build()) {
                        withWebsocketCredentialsProvider(defaultProvider);
                    }
                }

                try (AwsSigningConfig signingConfig = new AwsSigningConfig()) {
                    signingConfig.setAlgorithm(AwsSigningConfig.AwsSigningAlgorithm.SIGV4);
                    signingConfig.setSignatureType(AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS);
                    signingConfig.setRegion(websocketSigningRegion);
                    signingConfig.setService(IOT_SIGNING_SERVICE);
                    signingConfig.setCredentialsProvider(websocketCredentialsProvider);
                    signingConfig.setOmitSessionToken(true);

                    try (AwsSigv4HandshakeTransformer transformer = new AwsSigv4HandshakeTransformer(signingConfig)) {
                        connectionConfig.setWebsocketHandshakeTransform(transformer);

                        /*
                         * transformer is actually a CrtResource since we track a SigningConfig (which tracks a Credentials Provider
                         * But the MqttConnectionConfig only knows of the transformer as a Consumer function, so it's not
                         * able to properly add a forward reference to the transformer.  So we do it manually here after setting.
                         */
                        connectionConfig.addReferenceTo(transformer);
                    }
                }
            }

            return new MqttClientConnection(connectionConfig);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy