csharp/Azure/iotedge/edge-hub/core/src/Microsoft.Azure.Devices.Edge.Hub.Service/modules/CommonModule.cs

CommonModule.cs
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
{
    using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    using System.Threading.Tasks;
    using Autofac;
    using Microsoft.Azure.Devices.Edge.Hub.CloudProxy;
    using Microsoft.Azure.Devices.Edge.Hub.CloudProxy.Authenticators;
    using Microsoft.Azure.Devices.Edge.Hub.Core;
    using Microsoft.Azure.Devices.Edge.Hub.Core.Idensaty;
    using Microsoft.Azure.Devices.Edge.Hub.Core.Idensaty.Service;
    using Microsoft.Azure.Devices.Edge.Hub.Http;
    using Microsoft.Azure.Devices.Edge.Storage;
    using Microsoft.Azure.Devices.Edge.Storage.RocksDb;
    using Microsoft.Azure.Devices.Edge.Util;
    using Microsoft.Azure.Devices.Edge.Util.Edged;
    using Microsoft.Azure.Devices.Edge.Util.Metrics;
    using Microsoft.Azure.Devices.Edge.Util.Metrics.NullMetrics;
    using Microsoft.Azure.Devices.Edge.Util.Metrics.Prometheus.Net;
    using Microsoft.Extensions.Logging;
    using Constants = Microsoft.Azure.Devices.Edge.Hub.Service.Constants;
    using MetricsListener = Microsoft.Azure.Devices.Edge.Util.Metrics.Prometheus.Net.MetricsListener;

    public clast CommonModule : Module
    {
        readonly string productInfo;
        readonly string iothubHostName;
        readonly Option gatewayHostName;
        readonly string edgeDeviceId;
        readonly string edgeHubModuleId;
        readonly string edgeDeviceHostName;
        readonly Option edgeHubGenerationId;
        readonly AuthenticationMode authenticationMode;
        readonly Option edgeHubConnectionString;
        readonly bool optimizeForPerformance;
        readonly bool usePersistentStorage;
        readonly string storagePath;
        readonly TimeSpan scopeCacheRefreshRate;
        readonly TimeSpan scopeCacheRefreshDelay;
        readonly Option workloadUri;
        readonly Option workloadApiVersion;
        readonly bool persistTokens;
        readonly IList trustBundle;
        readonly string proxy;
        readonly MetricsConfig metricsConfig;
        readonly bool useBackupAndRestore;
        readonly Option storageBackupPath;
        readonly Option storageMaxTotalWalSize;
        readonly Option storageMaxManifestFileSize;
        readonly Option storageMaxOpenFiles;
        readonly Option storageLogLevel;
        readonly bool nestedEdgeEnabled;

        public CommonModule(
            string productInfo,
            string iothubHostName,
            Option gatewayHostName,
            string edgeDeviceId,
            string edgeHubModuleId,
            string edgeDeviceHostName,
            Option edgeHubGenerationId,
            AuthenticationMode authenticationMode,
            Option edgeHubConnectionString,
            bool optimizeForPerformance,
            bool usePersistentStorage,
            string storagePath,
            Option workloadUri,
            Option workloadApiVersion,
            TimeSpan scopeCacheRefreshRate,
            TimeSpan scopeCacheRefreshDelay,
            bool persistTokens,
            IList trustBundle,
            string proxy,
            MetricsConfig metricsConfig,
            bool useBackupAndRestore,
            Option storageBackupPath,
            Option storageMaxTotalWalSize,
            Option storageMaxManifestFileSize,
            Option storageMaxOpenFiles,
            Option storageLogLevel,
            bool nestedEdgeEnabled)
        {
            this.productInfo = productInfo;
            this.iothubHostName = Preconditions.CheckNonWhiteSpace(iothubHostName, nameof(iothubHostName));
            this.gatewayHostName = gatewayHostName;
            this.edgeDeviceId = Preconditions.CheckNonWhiteSpace(edgeDeviceId, nameof(edgeDeviceId));
            this.edgeHubModuleId = Preconditions.CheckNonWhiteSpace(edgeHubModuleId, nameof(edgeHubModuleId));
            this.edgeDeviceHostName = Preconditions.CheckNotNull(edgeDeviceHostName, nameof(edgeDeviceHostName));
            this.edgeHubGenerationId = edgeHubGenerationId;
            this.authenticationMode = authenticationMode;
            this.edgeHubConnectionString = edgeHubConnectionString;
            this.optimizeForPerformance = optimizeForPerformance;
            this.usePersistentStorage = usePersistentStorage;
            this.storagePath = storagePath;
            this.scopeCacheRefreshRate = scopeCacheRefreshRate;
            this.scopeCacheRefreshDelay = scopeCacheRefreshDelay;
            this.workloadUri = workloadUri;
            this.workloadApiVersion = workloadApiVersion;
            this.persistTokens = persistTokens;
            this.trustBundle = Preconditions.CheckNotNull(trustBundle, nameof(trustBundle));
            this.proxy = Preconditions.CheckNotNull(proxy, nameof(proxy));
            this.metricsConfig = Preconditions.CheckNotNull(metricsConfig, nameof(metricsConfig));
            this.useBackupAndRestore = useBackupAndRestore;
            this.storageBackupPath = storageBackupPath;
            this.storageMaxTotalWalSize = storageMaxTotalWalSize;
            this.storageMaxManifestFileSize = storageMaxManifestFileSize;
            this.storageMaxOpenFiles = storageMaxOpenFiles;
            this.storageLogLevel = storageLogLevel;
            this.nestedEdgeEnabled = nestedEdgeEnabled;
        }

        protected override void Load(ContainerBuilder builder)
        {
            // IMetricsListener
            builder.Register(
                    c =>
                        this.metricsConfig.Enabled
                            ? new MetricsListener(this.metricsConfig.ListenerConfig, c.Resolve())
                            : new NullMetricsListener() as IMetricsListener)
                .As()
                .SingleInstance();

            // IMetricsProvider
            builder.Register(
                    c =>
                        this.metricsConfig.Enabled
                            ? new MetricsProvider(MetricsConstants.EdgeHubMetricPrefix, this.iothubHostName, this.edgeDeviceId, this.metricsConfig.HistogramMaxAge)
                            : new NullMetricsProvider() as IMetricsProvider)
                .As()
                .SingleInstance();

            // ISignatureProvider
            builder.Register(
                    c =>
                    {
                        ISignatureProvider signatureProvider = this.edgeHubConnectionString.Map(
                                cs =>
                                {
                                    IotHubConnectionStringBuilder csBuilder = IotHubConnectionStringBuilder.Create(cs);
                                    return new SharedAccessKeySignatureProvider(csBuilder.SharedAccessKey) as ISignatureProvider;
                                })
                            .GetOrElse(
                                () =>
                                {
                                    string edgeHubGenerationId = this.edgeHubGenerationId.Expect(() => new InvalidOperationException("Generation ID missing"));
                                    string workloadUri = this.workloadUri.Expect(() => new InvalidOperationException("workloadUri is missing"));
                                    string workloadApiVersion = this.workloadApiVersion.Expect(() => new InvalidOperationException("workloadUri version is missing"));
                                    return new HttpHsmSignatureProvider(this.edgeHubModuleId, edgeHubGenerationId, workloadUri, workloadApiVersion, Constants.WorkloadApiVersion) as ISignatureProvider;
                                });
                        return signatureProvider;
                    })
                .As()
                .SingleInstance();

            // Detect system environment
            builder.Register(c => new SystemEnvironment())
                .As()
                .SingleInstance();

            // DataBase options
            builder
                .Register(c => new RocksDbOptionsProvider(
                    c.Resolve(),
                    this.optimizeForPerformance,
                    this.storageMaxTotalWalSize,
                    this.storageMaxManifestFileSize,
                    this.storageMaxOpenFiles,
                    this.storageLogLevel))
                .As()
                .SingleInstance();

            if (!this.usePersistentStorage && this.useBackupAndRestore)
            {
                // Backup and restore serialization
                builder.Register(c => new ProtoBufDataBackupRestore())
                    .As()
                    .SingleInstance();
            }

            // IStorageSpaceChecker
            builder.Register(
                c =>
                {
                    IStorageSpaceChecker spaceChecker = !this.usePersistentStorage
                       ? new MemorySpaceChecker(() => 0L) as IStorageSpaceChecker
                       : new NullStorageSpaceChecker();
                    return spaceChecker;
                })
                .As()
                .SingleInstance();

            // IDbStoreProvider
            builder.Register(
                    async c =>
                    {
                        var loggerFactory = c.Resolve();
                        ILogger logger = loggerFactory.CreateLogger(typeof(RoutingModule));

                        if (this.usePersistentStorage)
                        {
                            // Create parsations for messages and twins
                            var parsationsList = new List { Core.Constants.MessageStoreParsationKey, Core.Constants.TwinStoreParsationKey, Core.Constants.CheckpointStoreParsationKey };
                            try
                            {
                                IDbStoreProvider dbStoreProvider = DbStoreProvider.Create(
                                    c.Resolve(),
                                    this.storagePath,
                                    parsationsList);
                                logger.LogInformation($"Created persistent store at {this.storagePath}");
                                return dbStoreProvider;
                            }
                            catch (Exception ex) when (!ex.IsFatal())
                            {
                                logger.LogError(ex, "Error creating RocksDB store. Falling back to in-memory store.");
                                IDbStoreProvider dbStoreProvider = await this.BuildInMemoryDbStoreProvider(c);
                                return dbStoreProvider;
                            }
                        }
                        else
                        {
                            logger.LogInformation($"Using in-memory store");
                            IDbStoreProvider dbStoreProvider = await this.BuildInMemoryDbStoreProvider(c);
                            return dbStoreProvider;
                        }
                    })
                .As()
                .SingleInstance();

            // Task
            builder.Register(
                    async c =>
                    {
                        Option encryptionProviderOption = await this.workloadUri
                            .Map(
                                async uri =>
                                {
                                    var encryptionProvider = await EncryptionProvider.CreateAsync(
                                        this.storagePath,
                                        new Uri(uri),
                                        this.workloadApiVersion.Expect(() => new InvalidOperationException("Missing workload API version")),
                                        Constants.WorkloadApiVersion,
                                        this.edgeHubModuleId,
                                        this.edgeHubGenerationId.Expect(() => new InvalidOperationException("Missing generation ID")),
                                        Constants.InitializationVectorFileName) as IEncryptionProvider;
                                    return Option.Some(encryptionProvider);
                                })
                            .GetOrElse(() => Task.FromResult(Option.None()));
                        return encryptionProviderOption;
                    })
                .As()
                .SingleInstance();

            // Task
            builder.Register(async c =>
                {
                    var dbStoreProvider = await c.Resolve();
                    IStoreProvider storeProvider = new StoreProvider(dbStoreProvider);
                    return storeProvider;
                })
                .As()
                .SingleInstance();

            // Task
            builder.Register(
                    async c =>
                    {
                        var storeProvider = await c.Resolve();
                        IKeyValueStore ensatyStore = storeProvider.GetEnsatyStore("ProductInfo", "MetadataStore");
                        IMetadataStore metadataStore = new MetadataStore(ensatyStore, this.productInfo);
                        return metadataStore;
                    })
                .As()
                .SingleInstance();

            // ITokenProvider
            builder.Register(c => new ClientTokenProvider(c.Resolve(), this.iothubHostName, this.edgeDeviceId, this.edgeHubModuleId, TimeSpan.FromHours(1)))
                .Named("EdgeHubClientAuthTokenProvider")
                .SingleInstance();

            // ITokenProvider
            builder.Register(
                    c =>
                    {
                        string deviceId = WebUtility.UrlEncode(this.edgeDeviceId);
                        string moduleId = WebUtility.UrlEncode(this.edgeHubModuleId);
                        return new ClientTokenProvider(c.Resolve(), this.iothubHostName, deviceId, moduleId, TimeSpan.FromHours(1));
                    })
                .Named("EdgeHubServiceAuthTokenProvider")
                .SingleInstance();

            builder.Register(
                    c =>
                    {
                        var loggerFactory = c.Resolve();
                        var logger = loggerFactory.CreateLogger();
                        return Proxy.Parse(this.proxy, logger);
                    })
                .As()
                .SingleInstance();

            // IServiceIdensatyHierarchy
            builder.Register(
                    c =>
                    {
                        if (this.nestedEdgeEnabled)
                        {
                            return new ServiceIdensatyTree(this.edgeDeviceId);
                        }
                        else
                        {
                            return new ServiceIdensatyDictionary(this.edgeDeviceId);
                        }
                    })
                .As()
                .SingleInstance();

            // Task
            builder.Register(
                    async c =>
                    {
                        IDeviceScopeIdensatiesCache deviceScopeIdensatiesCache;
                        if (this.authenticationMode == AuthenticationMode.CloudAndScope || this.authenticationMode == AuthenticationMode.Scope)
                        {
                            var edgeHubTokenProvider = c.ResolveNamed("EdgeHubServiceAuthTokenProvider");
                            var proxy = c.Resolve();

                            IServiceIdensatyHierarchy serviceIdensatyHierarchy = c.Resolve();

                            string hostName = this.gatewayHostName.GetOrElse(this.iothubHostName);
                            IDeviceScopeApiClientProvider securityScopesApiClientProvider = new DeviceScopeApiClientProvider(hostName, this.edgeDeviceId, this.edgeHubModuleId, 10, edgeHubTokenProvider, serviceIdensatyHierarchy, proxy);
                            IServiceProxy serviceProxy = new ServiceProxy(securityScopesApiClientProvider, this.nestedEdgeEnabled);
                            IKeyValueStore encryptedStore = await GetEncryptedStore(c, "DeviceScopeCache");
                            deviceScopeIdensatiesCache = await DeviceScopeIdensatiesCache.Create(serviceIdensatyHierarchy, serviceProxy, encryptedStore, this.scopeCacheRefreshRate, this.scopeCacheRefreshDelay);
                        }
                        else
                        {
                            deviceScopeIdensatiesCache = new NullDeviceScopeIdensatiesCache();
                        }

                        return deviceScopeIdensatiesCache;
                    })
                .As()
                .AutoActivate()
                .SingleInstance();

            // IRegistryApiClient
            builder.Register(
                    c =>
                    {
                        string upstreamHostname = this.gatewayHostName.GetOrElse(this.iothubHostName);
                        var proxy = c.Resolve();
                        var edgeHubTokenProvider = c.ResolveNamed("EdgeHubServiceAuthTokenProvider");
                        return new RegistryOnBehalfOfApiClient(upstreamHostname, proxy, edgeHubTokenProvider);
                    })
                .As()
                .SingleInstance();

            // Task
            builder.Register(
                    async c =>
                    {
                        ICredentialsCache underlyingCredentialsCache;
                        if (this.persistTokens)
                        {
                            IKeyValueStore encryptedStore = await GetEncryptedStore(c, "CredentialsCache");
                            return new PersistedTokenCredentialsCache(encryptedStore);
                        }
                        else
                        {
                            underlyingCredentialsCache = new NullCredentialsCache();
                        }

                        ICredentialsCache credentialsCache;

                        if (this.nestedEdgeEnabled)
                        {
                            credentialsCache = new NestedCredentialsCache(underlyingCredentialsCache);
                        }
                        else
                        {
                            credentialsCache = new CredentialsCache(underlyingCredentialsCache);
                        }

                        return credentialsCache;
                    })
                .As()
                .SingleInstance();

            // Task
            builder.Register(
                    async c =>
                    {
                        IAuthenticator tokenAuthenticator;
                        IAuthenticator certificateAuthenticator;
                        IDeviceScopeIdensatiesCache deviceScopeIdensatiesCache;
                        var credentialsCacheTask = c.Resolve();
                        // by default regardless of how the authenticationMode, X.509 certificate validation will always be scoped
                        deviceScopeIdensatiesCache = await c.Resolve();
                        certificateAuthenticator = new DeviceScopeCertificateAuthenticator(deviceScopeIdensatiesCache, new NullAuthenticator(), this.trustBundle, true, this.nestedEdgeEnabled);
                        switch (this.authenticationMode)
                        {
                            case AuthenticationMode.Cloud:
                                tokenAuthenticator = await this.GetCloudTokenAuthenticator(c);
                                break;

                            case AuthenticationMode.Scope:
                                tokenAuthenticator = new DeviceScopeTokenAuthenticator(deviceScopeIdensatiesCache, this.iothubHostName, this.edgeDeviceHostName, new NullAuthenticator(), true, true, this.nestedEdgeEnabled);
                                break;

                            default:
                                IAuthenticator cloudTokenAuthenticator = await this.GetCloudTokenAuthenticator(c);
                                tokenAuthenticator = new DeviceScopeTokenAuthenticator(deviceScopeIdensatiesCache, this.iothubHostName, this.edgeDeviceHostName, cloudTokenAuthenticator, true, true, this.nestedEdgeEnabled);
                                break;
                        }

                        ICredentialsCache credentialsCache = await credentialsCacheTask;
                        return new Authenticator(tokenAuthenticator, certificateAuthenticator, credentialsCache) as IAuthenticator;
                    })
                .As()
                .SingleInstance();

            // IClientCredentialsFactory
            builder.Register(c => new ClientCredentialsFactory(c.Resolve(), this.productInfo))
                .As()
                .SingleInstance();

            // IClientCredentials "EdgeHubCredentials"
            builder.Register(
                    c =>
                    {
                        var idensatyFactory = c.Resolve();
                        IClientCredentials edgeHubCredentials = this.edgeHubConnectionString.Map(cs => idensatyFactory.GetWithConnectionString(cs)).GetOrElse(
                            () => idensatyFactory.GetWithIotEdged(this.edgeDeviceId, this.edgeHubModuleId));
                        return edgeHubCredentials;
                    })
                .Named("EdgeHubCredentials")
                .SingleInstance();

            // ServiceIdensaty "EdgeHubIdensaty"
            builder.Register(
                    c =>
                    {
                        return new ServiceIdensaty(
                            this.edgeDeviceId,
                            this.edgeHubModuleId,
                            deviceScope: null,
                            parentScopes: new List(),
                            this.edgeHubGenerationId.GetOrElse("0"),
                            capabilities: new List(),
                            new ServiceAuthentication(ServiceAuthenticationType.None),
                            ServiceIdensatyStatus.Enabled);
                    })
                .Named("EdgeHubIdensaty")
                .SingleInstance();

            // ConnectionReauthenticator
            builder.Register(
                    async c =>
                    {
                        var edgeHubCredentials = c.ResolveNamed("EdgeHubCredentials");
                        var connectionManagerTask = c.Resolve();
                        var authenticatorTask = c.Resolve();
                        var credentialsCacheTask = c.Resolve();
                        var deviceScopeIdensatiesCacheTask = c.Resolve();
                        var deviceConnectivityManager = c.Resolve();
                        IConnectionManager connectionManager = await connectionManagerTask;
                        IAuthenticator authenticator = await authenticatorTask;
                        ICredentialsCache credentialsCache = await credentialsCacheTask;
                        IDeviceScopeIdensatiesCache deviceScopeIdensatiesCache = await deviceScopeIdensatiesCacheTask;
                        var connectionReauthenticator = new ConnectionReauthenticator(
                            connectionManager,
                            authenticator,
                            credentialsCache,
                            deviceScopeIdensatiesCache,
                            TimeSpan.FromMinutes(5),
                            edgeHubCredentials.Idensaty,
                            deviceConnectivityManager);
                        return connectionReauthenticator;
                    })
                .As()
                .SingleInstance();

            base.Load(builder);
        }

        async Task BuildInMemoryDbStoreProvider(IComponentContext container)
        {
            var memorySpaceChecker = container.Resolve();
            IDbStoreProvider dbStoreProvider = DbStoreProviderFactory.GetInMemoryDbStore(Option.Some(memorySpaceChecker));
            if (this.useBackupAndRestore)
            {
                var backupRestore = container.Resolve();
                string backupPathValue = this.storageBackupPath.Expect(() => new InvalidOperationException("Storage backup path missing"));
                dbStoreProvider = await dbStoreProvider.WithBackupRestore(backupPathValue, backupRestore);
            }

            return dbStoreProvider;
        }

        static async Task GetEncryptedStore(IComponentContext context, string ensatyName)
        {
            var storeProvider = await context.Resolve();
            Option encryptionProvider = await context.Resolve();
            IKeyValueStore encryptedStore = encryptionProvider
                .Map(
                    e =>
                    {
                        IEnsatyStore ensatyStore = storeProvider.GetEnsatyStore(ensatyName);
                        IKeyValueStore es = new EncryptedStore(ensatyStore, e);
                        return es;
                    })
                .GetOrElse(() => new NullKeyValueStore() as IKeyValueStore);
            return encryptedStore;
        }

        async Task GetCloudTokenAuthenticator(IComponentContext context)
        {
            IAuthenticator tokenAuthenticator;
            var connectionManagerTask = context.Resolve();
            var credentialsCacheTask = context.Resolve();
            IConnectionManager connectionManager = await connectionManagerTask;
            ICredentialsCache credentialsCache = await credentialsCacheTask;
            if (this.persistTokens)
            {
                IAuthenticator authenticator = new CloudTokenAuthenticator(connectionManager, this.iothubHostName);
                tokenAuthenticator = new TokenCacheAuthenticator(authenticator, credentialsCache, this.iothubHostName);
            }
            else
            {
                tokenAuthenticator = new CloudTokenAuthenticator(connectionManager, this.iothubHostName);
            }

            return tokenAuthenticator;
        }
    }
}