Skip to content

Commit

Permalink
Use HTTPS proxy in Edge Hub (#697)
Browse files Browse the repository at this point in the history
In Linux, .NET Core recognizes and honors the `https_proxy` environment variable when it is present in a module like Edge Agent or Edge Hub. The behavior is different in Windows, however, where the default WinInet proxy settings are used (as set via the Control Panel, or Internet Explorer) and `https_proxy` is ignored. This is especially a problem in RS5 nanoserver containers, which don't even expose the WinInet proxy settings.

The fix is to look for the environment variable ourselves, create a `WebProxy` object, and attach it to the `ITransportSettings` object that we pass into the SDK's `ModuleClient`. We technically only have to do this for Windows, but we'll do it for all platforms, for consistency.

This change updates Edge Hub. Edge Agent was updated previously (see #687). Note that for Edge Hub, we have to pass the proxy info into our own `System.Net.Http.HttpClient`-based `DeviceScopeApiClient` as well as `ModuleClient`.
  • Loading branch information
damonbarry authored Jan 10, 2019
1 parent cdd9be4 commit eb75f34
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ namespace Microsoft.Azure.Devices.Edge.Hub.CloudProxy
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Client.Transport.Mqtt;
Expand All @@ -18,14 +20,6 @@ public class CloudConnectionProvider : ICloudConnectionProvider
// Minimum value allowed by the SDK for Connection Idle timeout for AMQP Multiplexed connections.
static readonly TimeSpan MinAmqpConnectionMuxIdleTimeout = TimeSpan.FromSeconds(5);

static readonly IDictionary<UpstreamProtocol, TransportType> UpstreamProtocolTransportTypeMap = new Dictionary<UpstreamProtocol, TransportType>
{
[UpstreamProtocol.Amqp] = TransportType.Amqp_Tcp_Only,
[UpstreamProtocol.AmqpWs] = TransportType.Amqp_WebSocket_Only,
[UpstreamProtocol.Mqtt] = TransportType.Mqtt_Tcp_Only,
[UpstreamProtocol.MqttWs] = TransportType.Mqtt_WebSocket_Only
};

readonly ITransportSettings[] transportSettings;
readonly IMessageConverterProvider messageConverterProvider;
readonly IClientProvider clientProvider;
Expand All @@ -49,12 +43,13 @@ public CloudConnectionProvider(
IIdentity edgeHubIdentity,
TimeSpan idleTimeout,
bool closeOnIdleTimeout,
TimeSpan operationTimeout)
TimeSpan operationTimeout,
Option<IWebProxy> proxy)
{
Preconditions.CheckRange(connectionPoolSize, 1, nameof(connectionPoolSize));
this.messageConverterProvider = Preconditions.CheckNotNull(messageConverterProvider, nameof(messageConverterProvider));
this.clientProvider = Preconditions.CheckNotNull(clientProvider, nameof(clientProvider));
this.transportSettings = GetTransportSettings(upstreamProtocol, connectionPoolSize);
this.transportSettings = GetTransportSettings(upstreamProtocol, connectionPoolSize, proxy);
this.edgeHub = Option.None<IEdgeHub>();
this.idleTimeout = idleTimeout;
this.closeOnIdleTimeout = closeOnIdleTimeout;
Expand Down Expand Up @@ -167,54 +162,54 @@ public async Task<Try<ICloudConnection>> Connect(IIdentity identity, Action<stri
}
}

internal static ITransportSettings[] GetTransportSettings(Option<UpstreamProtocol> upstreamProtocol, int connectionPoolSize)
static ITransportSettings[] GetAmqpTransportSettings(TransportType type, int connectionPoolSize, Option<IWebProxy> proxy)
{
var settings = new AmqpTransportSettings(type)
{
AmqpConnectionPoolSettings = new AmqpConnectionPoolSettings
{
Pooling = true,
MaxPoolSize = (uint)connectionPoolSize,
ConnectionIdleTimeout = MinAmqpConnectionMuxIdleTimeout
}
};
proxy.ForEach(p => settings.Proxy = p);
return new ITransportSettings[] { settings };
}

static ITransportSettings[] GetMqttTransportSettings(TransportType type, Option<IWebProxy> proxy)
{
var settings = new MqttTransportSettings(type);
proxy.ForEach(p => settings.Proxy = p);
return new ITransportSettings[] { settings };
}

internal static ITransportSettings[] GetTransportSettings(Option<UpstreamProtocol> upstreamProtocol, int connectionPoolSize, Option<IWebProxy> proxy)
{
return upstreamProtocol
.Map(
up =>
{
TransportType transportType = UpstreamProtocolTransportTypeMap[up];
switch (transportType)
switch (up)
{
case TransportType.Amqp_Tcp_Only:
case TransportType.Amqp_WebSocket_Only:
return new ITransportSettings[]
{
new AmqpTransportSettings(transportType)
{
AmqpConnectionPoolSettings = new AmqpConnectionPoolSettings
{
Pooling = true,
MaxPoolSize = (uint)connectionPoolSize,
ConnectionIdleTimeout = MinAmqpConnectionMuxIdleTimeout
}
}
};
case UpstreamProtocol.Amqp:
return GetAmqpTransportSettings(TransportType.Amqp_Tcp_Only, connectionPoolSize, proxy);
case UpstreamProtocol.AmqpWs:
return GetAmqpTransportSettings(TransportType.Amqp_WebSocket_Only, connectionPoolSize, proxy);
case TransportType.Mqtt_Tcp_Only:
case TransportType.Mqtt_WebSocket_Only:
return new ITransportSettings[]
{
new MqttTransportSettings(transportType)
};
case UpstreamProtocol.Mqtt:
return GetMqttTransportSettings(TransportType.Mqtt_Tcp_Only, proxy);
case UpstreamProtocol.MqttWs:
return GetMqttTransportSettings(TransportType.Mqtt_WebSocket_Only, proxy);
default:
throw new ArgumentException($"Unsupported transport type {up}");
throw new InvalidEnumArgumentException($"Unsupported transport type {up}");
}
})
.GetOrElse(
() => new ITransportSettings[]
{
new AmqpTransportSettings(TransportType.Amqp_Tcp_Only)
{
AmqpConnectionPoolSettings = new AmqpConnectionPoolSettings
{
Pooling = true,
MaxPoolSize = (uint)connectionPoolSize,
ConnectionIdleTimeout = MinAmqpConnectionMuxIdleTimeout
}
}
});
() => GetAmqpTransportSettings(TransportType.Amqp_Tcp_Only, connectionPoolSize, proxy));
}

static class Events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ public class DeviceScopeApiClient : IDeviceScopeApiClient
readonly string moduleId;
readonly int batchSize;
readonly ITokenProvider edgeHubTokenProvider;
readonly Option<IWebProxy> proxy;

public DeviceScopeApiClient(
string iotHubHostName,
string deviceId,
string moduleId,
int batchSize,
ITokenProvider edgeHubTokenProvider,
Option<IWebProxy> proxy,
RetryStrategy retryStrategy = null)
{
Preconditions.CheckNonWhiteSpace(iotHubHostName, nameof(iotHubHostName));
Expand All @@ -49,6 +51,7 @@ public DeviceScopeApiClient(
this.moduleId = Preconditions.CheckNonWhiteSpace(moduleId, nameof(moduleId));
this.batchSize = Preconditions.CheckRange(batchSize, 0, 1000, nameof(batchSize));
this.edgeHubTokenProvider = Preconditions.CheckNotNull(edgeHubTokenProvider, nameof(edgeHubTokenProvider));
this.proxy = Preconditions.CheckNotNull(proxy, nameof(proxy));
this.retryStrategy = retryStrategy ?? TransientRetryStrategy;
}

Expand Down Expand Up @@ -107,7 +110,9 @@ async Task<ScopeResult> GetIdentitiesInScopeWithRetry(Uri uri)

async Task<ScopeResult> GetIdentitiesInScope(Uri uri)
{
var client = new HttpClient();
HttpClient client = this.proxy
.Map(p => new HttpClient(new HttpClientHandler { Proxy = p }, disposeHandler: true))
.GetOrElse(() => new HttpClient());
using (var msg = new HttpRequestMessage(HttpMethod.Get, uri))
{
string token = await this.edgeHubTokenProvider.GetTokenAsync(Option.None<TimeSpan>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public DependencyManager(IConfigurationRoot configuration, X509Certificate2 serv
this.iotHubHostname = iotHubConnectionStringBuilder.HostName;
this.edgeDeviceId = iotHubConnectionStringBuilder.DeviceId;
this.edgeModuleId = iotHubConnectionStringBuilder.ModuleId;
this.edgeDeviceHostName = this.configuration.GetValue<string>(Constants.ConfigKey.EdgeDeviceHostName, string.Empty);
this.edgeDeviceHostName = this.configuration.GetValue(Constants.ConfigKey.EdgeDeviceHostName, string.Empty);
this.connectionString = Option.Some(edgeHubConnectionString);
}
else
Expand Down Expand Up @@ -159,6 +159,8 @@ void RegisterCommonModule(ContainerBuilder builder, bool optimizeForPerformance,
int scopeCacheRefreshRateSecs = this.configuration.GetValue("DeviceScopeCacheRefreshRateSecs", 3600);
TimeSpan scopeCacheRefreshRate = TimeSpan.FromSeconds(scopeCacheRefreshRateSecs);

string proxy = this.configuration.GetValue("https_proxy", string.Empty);

// Register modules
builder.RegisterModule(
new CommonModule(
Expand All @@ -176,7 +178,8 @@ void RegisterCommonModule(ContainerBuilder builder, bool optimizeForPerformance,
workloadUri,
scopeCacheRefreshRate,
cacheTokens,
this.trustBundle));
this.trustBundle,
proxy));
}

(bool isEnabled, bool usePersistentStorage, StoreAndForwardConfiguration config, string storagePath) GetStoreAndForwardConfiguration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class CommonModule : Module
readonly Option<string> workloadUri;
readonly bool persistTokens;
readonly IList<X509Certificate2> trustBundle;
readonly string proxy;

public CommonModule(
string productInfo,
Expand All @@ -51,7 +52,8 @@ public CommonModule(
Option<string> workloadUri,
TimeSpan scopeCacheRefreshRate,
bool persistTokens,
IList<X509Certificate2> trustBundle)
IList<X509Certificate2> trustBundle,
string proxy)
{
this.productInfo = productInfo;
this.iothubHostName = Preconditions.CheckNonWhiteSpace(iothubHostName, nameof(iothubHostName));
Expand All @@ -68,6 +70,7 @@ public CommonModule(
this.workloadUri = workloadUri;
this.persistTokens = persistTokens;
this.trustBundle = Preconditions.CheckNotNull(trustBundle, nameof(trustBundle));
this.proxy = Preconditions.CheckNotNull(proxy, nameof(proxy));
}

protected override void Load(ContainerBuilder builder)
Expand Down Expand Up @@ -183,6 +186,16 @@ protected override void Load(ContainerBuilder builder)
.Named<ITokenProvider>("EdgeHubServiceAuthTokenProvider")
.SingleInstance();

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

// Task<IDeviceScopeIdentitiesCache>
builder.Register(
async c =>
Expand All @@ -191,7 +204,8 @@ protected override void Load(ContainerBuilder builder)
if (this.authenticationMode == AuthenticationMode.CloudAndScope || this.authenticationMode == AuthenticationMode.Scope)
{
var edgeHubTokenProvider = c.ResolveNamed<ITokenProvider>("EdgeHubServiceAuthTokenProvider");
IDeviceScopeApiClient securityScopesApiClient = new DeviceScopeApiClient(this.iothubHostName, this.edgeDeviceId, this.edgeHubModuleId, 10, edgeHubTokenProvider);
var proxy = c.Resolve<Option<IWebProxy>>();
IDeviceScopeApiClient securityScopesApiClient = new DeviceScopeApiClient(this.iothubHostName, this.edgeDeviceId, this.edgeHubModuleId, 10, edgeHubTokenProvider, proxy);
IServiceProxy serviceProxy = new ServiceProxy(securityScopesApiClient);
IKeyValueStore<string, string> encryptedStore = await GetEncryptedStore(c, "DeviceScopeCache");
deviceScopeIdentitiesCache = await DeviceScopeIdentitiesCache.Create(serviceProxy, encryptedStore, this.scopeCacheRefreshRate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Autofac;
using Microsoft.Azure.Devices.Edge.Hub.CloudProxy;
Expand All @@ -20,6 +21,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.Service.Modules
using Microsoft.Azure.Devices.Routing.Core.Checkpointers;
using Microsoft.Azure.Devices.Routing.Core.Endpoints;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Extensions.Logging;
using IRoutingMessage = Microsoft.Azure.Devices.Routing.Core.IMessage;
using Message = Microsoft.Azure.Devices.Client.Message;

Expand Down Expand Up @@ -177,6 +179,7 @@ protected override void Load(ContainerBuilder builder)
var credentialsCacheTask = c.Resolve<Task<ICredentialsCache>>();
var edgeHubCredentials = c.ResolveNamed<IClientCredentials>("EdgeHubCredentials");
var deviceScopeIdentitiesCacheTask = c.Resolve<Task<IDeviceScopeIdentitiesCache>>();
var proxy = c.Resolve<Option<IWebProxy>>();
IDeviceScopeIdentitiesCache deviceScopeIdentitiesCache = await deviceScopeIdentitiesCacheTask;
ICredentialsCache credentialsCache = await credentialsCacheTask;
ICloudConnectionProvider cloudConnectionProvider = new CloudConnectionProvider(
Expand All @@ -190,7 +193,8 @@ protected override void Load(ContainerBuilder builder)
edgeHubCredentials.Identity,
this.cloudConnectionIdleTimeout,
this.closeCloudConnectionOnIdleTimeout,
this.operationTimeout);
this.operationTimeout,
proxy);
return cloudConnectionProvider;
})
.As<Task<ICloudConnectionProvider>>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Microsoft.Azure.Devices.Edge.Hub.CloudProxy.Test
{
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Client.Exceptions;
Expand Down Expand Up @@ -424,7 +425,8 @@ IClient GetMockedDeviceClient()
Mock.Of<IIdentity>(i => i.Id == $"{deviceId}/$edgeHub"),
TimeSpan.FromMinutes(60),
true,
TimeSpan.FromSeconds(20));
TimeSpan.FromSeconds(20),
Option.None<IWebProxy>());
cloudConnectionProvider.BindEdgeHub(Mock.Of<IEdgeHub>());
IConnectionManager connectionManager = new ConnectionManager(cloudConnectionProvider, Mock.Of<ICredentialsCache>(), new IdentityProvider(hostname));

Expand Down
Loading

0 comments on commit eb75f34

Please sign in to comment.