Create HttpProxySettingsProvider and fixed related issues.

pull/1115/head
Taloth Saldono 9 years ago
parent f807e44a39
commit 9e7927acec

@ -20,7 +20,8 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Logger _logger = NzbDroneLogger.GetLogger(typeof(CurlHttpDispatcher)); private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly Logger _logger;
private const string _caBundleFileName = "curl-ca-bundle.crt"; private const string _caBundleFileName = "curl-ca-bundle.crt";
private static readonly string _caBundleFilePath; private static readonly string _caBundleFilePath;
@ -36,8 +37,14 @@ namespace NzbDrone.Common.Http.Dispatchers
_caBundleFilePath = _caBundleFileName; _caBundleFilePath = _caBundleFileName;
} }
} }
public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_logger = logger;
}
public static bool CheckAvailability() public bool CheckAvailability()
{ {
try try
{ {
@ -76,32 +83,10 @@ namespace NzbDrone.Common.Http.Dispatchers
return s * n; return s * n;
}; };
if(request.Proxy != null && !request.Proxy.ShouldProxyBeBypassed(new Uri(request.Url.FullUri))) AddProxy(curlEasy, request);
{
switch (request.Proxy.Type)
{
case ProxyType.Http:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Http);
curlEasy.SetOpt(CurlOption.ProxyAuth, CurlHttpAuth.Basic);
curlEasy.SetOpt(CurlOption.ProxyUserPwd, request.Proxy.Username + ":" + request.Proxy.Password.ToString());
break;
case ProxyType.Socks4:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks4);
curlEasy.SetOpt(CurlOption.ProxyUsername, request.Proxy.Username);
curlEasy.SetOpt(CurlOption.ProxyPassword, request.Proxy.Password);
break;
case ProxyType.Socks5:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks5);
curlEasy.SetOpt(CurlOption.ProxyUsername, request.Proxy.Username);
curlEasy.SetOpt(CurlOption.ProxyPassword, request.Proxy.Password);
break;
}
curlEasy.SetOpt(CurlOption.Proxy, request.Proxy.Host + ":" + request.Proxy.Port.ToString());
}
curlEasy.Url = request.Url.FullUri; curlEasy.Url = request.Url.FullUri;
switch (request.Method) switch (request.Method)
{ {
case HttpMethod.GET: case HttpMethod.GET:
@ -174,6 +159,34 @@ namespace NzbDrone.Common.Http.Dispatchers
} }
} }
private void AddProxy(CurlEasy curlEasy, HttpRequest request)
{
var proxySettings = _proxySettingsProvider.GetProxySettings(request);
if (proxySettings != null)
{
switch (proxySettings.Type)
{
case ProxyType.Http:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Http);
curlEasy.SetOpt(CurlOption.ProxyAuth, CurlHttpAuth.Basic);
curlEasy.SetOpt(CurlOption.ProxyUserPwd, proxySettings.Username + ":" + proxySettings.Password.ToString());
break;
case ProxyType.Socks4:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks4);
curlEasy.SetOpt(CurlOption.ProxyUsername, proxySettings.Username);
curlEasy.SetOpt(CurlOption.ProxyPassword, proxySettings.Password);
break;
case ProxyType.Socks5:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks5);
curlEasy.SetOpt(CurlOption.ProxyUsername, proxySettings.Username);
curlEasy.SetOpt(CurlOption.ProxyPassword, proxySettings.Password);
break;
}
curlEasy.SetOpt(CurlOption.Proxy, proxySettings.Host + ":" + proxySettings.Port.ToString());
}
}
private CurlSlist SerializeHeaders(HttpRequest request) private CurlSlist SerializeHeaders(HttpRequest request)
{ {
if (!request.Headers.ContainsKey("Accept-Encoding")) if (!request.Headers.ContainsKey("Accept-Encoding"))

@ -8,17 +8,18 @@ namespace NzbDrone.Common.Http.Dispatchers
{ {
public class FallbackHttpDispatcher : IHttpDispatcher public class FallbackHttpDispatcher : IHttpDispatcher
{ {
private readonly Logger _logger;
private readonly ICached<bool> _curlTLSFallbackCache;
private readonly ManagedHttpDispatcher _managedDispatcher; private readonly ManagedHttpDispatcher _managedDispatcher;
private readonly CurlHttpDispatcher _curlDispatcher; private readonly CurlHttpDispatcher _curlDispatcher;
private readonly Logger _logger;
private readonly ICached<bool> _curlTLSFallbackCache;
public FallbackHttpDispatcher(ICached<bool> curlTLSFallbackCache, Logger logger) public FallbackHttpDispatcher(ManagedHttpDispatcher managedDispatcher, CurlHttpDispatcher curlDispatcher, ICacheManager cacheManager, Logger logger)
{ {
_managedDispatcher = managedDispatcher;
_curlDispatcher = curlDispatcher;
_curlTLSFallbackCache = cacheManager.GetCache<bool>(GetType(), "curlTLSFallback");
_logger = logger; _logger = logger;
_curlTLSFallbackCache = curlTLSFallbackCache;
_managedDispatcher = new ManagedHttpDispatcher();
_curlDispatcher = new CurlHttpDispatcher();
} }
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
@ -46,7 +47,7 @@ namespace NzbDrone.Common.Http.Dispatchers
} }
} }
if (CurlHttpDispatcher.CheckAvailability()) if (_curlDispatcher.CheckAvailability())
{ {
return _curlDispatcher.GetResponse(request, cookies); return _curlDispatcher.GetResponse(request, cookies);
} }

@ -4,12 +4,22 @@ using NzbDrone.Common.Extensions;
using com.LandonKey.SocksWebProxy.Proxy; using com.LandonKey.SocksWebProxy.Proxy;
using com.LandonKey.SocksWebProxy; using com.LandonKey.SocksWebProxy;
using System.Net.Sockets; using System.Net.Sockets;
using System.Linq;
namespace NzbDrone.Common.Http.Dispatchers namespace NzbDrone.Common.Http.Dispatchers
{ {
public class ManagedHttpDispatcher : IHttpDispatcher public class ManagedHttpDispatcher : IHttpDispatcher
{ {
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICached<IWebProxy> _webProxyCache;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICacheManager cacheManager)
{
_proxySettingsProvider = proxySettingsProvider;
_webProxyCache = cacheManager.GetCache<IWebProxy>(GetType(), "webProxy");
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{ {
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
@ -30,42 +40,7 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
} }
if (request.Proxy != null && !request.Proxy.ShouldProxyBeBypassed(new Uri(request.Url.FullUri))) AddProxy(webRequest, request);
{
var addresses = Dns.GetHostAddresses(request.Proxy.Host);
if(addresses.Length > 1)
{
var ipv4Only = addresses.Where(a => a.AddressFamily == AddressFamily.InterNetwork);
if (ipv4Only.Any())
{
addresses = ipv4Only.ToArray();
}
}
var socksUsername = request.Proxy.Username == null ? string.Empty : request.Proxy.Username;
var socksPassword = request.Proxy.Password == null ? string.Empty : request.Proxy.Password;
switch (request.Proxy.Type)
{
case ProxyType.Http:
if(request.Proxy.Username.IsNotNullOrWhiteSpace() && request.Proxy.Password.IsNotNullOrWhiteSpace())
{
webRequest.Proxy = new WebProxy(request.Proxy.Host + ":" + request.Proxy.Port, request.Proxy.BypassLocalAddress, request.Proxy.SubnetFilterAsArray, new NetworkCredential(request.Proxy.Username, request.Proxy.Password));
}
else
{
webRequest.Proxy = new WebProxy(request.Proxy.Host + ":" + request.Proxy.Port, request.Proxy.BypassLocalAddress, request.Proxy.SubnetFilterAsArray);
}
break;
case ProxyType.Socks4:
webRequest.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Parse("127.0.0.1"), GetNextFreePort(), addresses[0], request.Proxy.Port, ProxyConfig.SocksVersion.Four, socksUsername, socksPassword), false);
break;
case ProxyType.Socks5:
webRequest.Proxy = new SocksWebProxy(new ProxyConfig(IPAddress.Parse("127.0.0.1"), GetNextFreePort(), addresses[0], request.Proxy.Port, ProxyConfig.SocksVersion.Five, socksUsername, socksPassword), false);
break;
}
}
if (request.Headers != null) if (request.Headers != null)
{ {
@ -110,6 +85,50 @@ namespace NzbDrone.Common.Http.Dispatchers
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode); return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
} }
protected virtual void AddProxy(HttpWebRequest webRequest, HttpRequest request)
{
var proxySettings = _proxySettingsProvider.GetProxySettings(request);
if (proxySettings != null)
{
webRequest.Proxy = _webProxyCache.Get(proxySettings.Key, () => CreateWebProxy(proxySettings), TimeSpan.FromMinutes(5));
}
_webProxyCache.ClearExpired();
}
private IWebProxy CreateWebProxy(HttpRequestProxySettings proxySettings)
{
var addresses = Dns.GetHostAddresses(proxySettings.Host);
if(addresses.Length > 1)
{
var ipv4Only = addresses.Where(a => a.AddressFamily == AddressFamily.InterNetwork);
if (ipv4Only.Any())
{
addresses = ipv4Only.ToArray();
}
}
switch (proxySettings.Type)
{
case ProxyType.Http:
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
{
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.SubnetFilterAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
}
else
{
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.SubnetFilterAsArray);
}
case ProxyType.Socks4:
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), addresses[0], proxySettings.Port, ProxyConfig.SocksVersion.Four, proxySettings.Username, proxySettings.Password), false);
case ProxyType.Socks5:
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), addresses[0], proxySettings.Port, ProxyConfig.SocksVersion.Five, proxySettings.Username, proxySettings.Password), false);
}
return null;
}
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers) protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers)
{ {
foreach (var header in headers) foreach (var header in headers)

@ -35,21 +35,15 @@ namespace NzbDrone.Common.Http
public HttpClient(IEnumerable<IHttpRequestInterceptor> requestInterceptors, ICacheManager cacheManager, IRateLimitService rateLimitService, IHttpDispatcher httpDispatcher, Logger logger) public HttpClient(IEnumerable<IHttpRequestInterceptor> requestInterceptors, ICacheManager cacheManager, IRateLimitService rateLimitService, IHttpDispatcher httpDispatcher, Logger logger)
{ {
_logger = logger;
_rateLimitService = rateLimitService;
_requestInterceptors = requestInterceptors.ToList(); _requestInterceptors = requestInterceptors.ToList();
ServicePointManager.DefaultConnectionLimit = 12; _rateLimitService = rateLimitService;
_httpDispatcher = httpDispatcher; _httpDispatcher = httpDispatcher;
_logger = logger;
ServicePointManager.DefaultConnectionLimit = 12;
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient)); _cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
} }
public HttpClient(IEnumerable<IHttpRequestInterceptor> requestInterceptors, ICacheManager cacheManager, IRateLimitService rateLimitService, Logger logger)
: this(requestInterceptors, cacheManager, rateLimitService, null, logger)
{
_httpDispatcher = new FallbackHttpDispatcher(cacheManager.GetCache<bool>(typeof(HttpClient), "curlTLSFallback"), _logger);
}
public HttpResponse Execute(HttpRequest request) public HttpResponse Execute(HttpRequest request)
{ {
foreach (var interceptor in _requestInterceptors) foreach (var interceptor in _requestInterceptors)

@ -41,7 +41,6 @@ namespace NzbDrone.Common.Http
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; } public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public HttpRequestProxySettings Proxy {get; set;}
public override string ToString() public override string ToString()
{ {

@ -1,18 +1,19 @@
using System; using System;
using System.Net; using System.Net;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public class HttpRequestProxySettings public class HttpRequestProxySettings
{ {
public HttpRequestProxySettings(ProxyType type, string host, int port, string filterSubnet, bool bypassLocalAddress, string username = null, string password = null) public HttpRequestProxySettings(ProxyType type, string host, int port, string bypassFilter, bool bypassLocalAddress, string username = null, string password = null)
{ {
Type = type; Type = type;
Host = host; Host = host.IsNullOrWhiteSpace() ? "127.0.0.1" : host;
Port = port; Port = port;
Username = username; Username = username ?? string.Empty;
Password = password; Password = password ?? string.Empty;
SubnetFilter = filterSubnet; BypassFilter = bypassFilter ?? string.Empty;
BypassLocalAddress = bypassLocalAddress; BypassLocalAddress = bypassLocalAddress;
} }
@ -21,27 +22,34 @@ namespace NzbDrone.Common.Http
public int Port { get; private set; } public int Port { get; private set; }
public string Username { get; private set; } public string Username { get; private set; }
public string Password { get; private set; } public string Password { get; private set; }
public string SubnetFilter { get; private set; } public string BypassFilter { get; private set; }
public bool BypassLocalAddress { get; private set; } public bool BypassLocalAddress { get; private set; }
public string[] SubnetFilterAsArray public string[] SubnetFilterAsArray
{ {
get get
{ {
if (!string.IsNullOrWhiteSpace(SubnetFilter)) if (!string.IsNullOrWhiteSpace(BypassFilter))
{ {
return SubnetFilter.Split(';'); return BypassFilter.Split(';');
} }
return new string[] { }; return new string[] { };
} }
} }
public bool ShouldProxyBeBypassed(Uri url) public string Key
{ {
//We are utilising the WebProxy implementation here to save us having to reimplement it. This way we use Microsofts implementation get
WebProxy proxy = new WebProxy(Host + ":" + Port, BypassLocalAddress, SubnetFilterAsArray); {
return string.Join("_",
return proxy.IsBypassed(url); Type,
Host,
Port,
Username,
Password,
BypassFilter,
BypassLocalAddress);
}
} }
} }
} }

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Common.Http
{
public interface IHttpProxySettingsProvider
{
HttpRequestProxySettings GetProxySettings(HttpRequest request);
}
}

@ -171,6 +171,7 @@
<Compile Include="Http\HttpRequestProxySettings.cs" /> <Compile Include="Http\HttpRequestProxySettings.cs" />
<Compile Include="Http\HttpResponse.cs" /> <Compile Include="Http\HttpResponse.cs" />
<Compile Include="Http\HttpUri.cs" /> <Compile Include="Http\HttpUri.cs" />
<Compile Include="Http\IHttpProxySettingsProvider.cs" />
<Compile Include="Http\IHttpRequestInterceptor.cs" /> <Compile Include="Http\IHttpRequestInterceptor.cs" />
<Compile Include="Http\JsonRpcRequestBuilder.cs" /> <Compile Include="Http\JsonRpcRequestBuilder.cs" />
<Compile Include="Http\JsonRpcResponse.cs" /> <Compile Include="Http\JsonRpcResponse.cs" />

@ -3,6 +3,7 @@ using NUnit.Framework;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@ -13,7 +14,7 @@ namespace NzbDrone.Core.Test.Framework
protected void UseRealHttp() protected void UseRealHttp()
{ {
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger)); Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), TestLogger));
Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder()); Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder());
} }
} }

@ -0,0 +1,52 @@
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Http
{
public class HttpProxySettingsProvider : IHttpProxySettingsProvider
{
private readonly IConfigService _configService;
public HttpProxySettingsProvider(IConfigService configService)
{
_configService = configService;
}
public HttpRequestProxySettings GetProxySettings(HttpRequest request)
{
if (!_configService.ProxyEnabled)
{
return null;
}
var proxySettings = new HttpRequestProxySettings(_configService.ProxyType,
_configService.ProxyHostname,
_configService.ProxyPort,
_configService.ProxySubnetFilter,
_configService.ProxyBypassLocalAddresses,
_configService.ProxyUsername,
_configService.ProxyPassword);
if (ShouldProxyBeBypassed(proxySettings, request.Url))
{
return null;
}
return proxySettings;
}
public bool ShouldProxyBeBypassed(HttpRequestProxySettings proxySettings, HttpUri url)
{
//We are utilising the WebProxy implementation here to save us having to reimplement it. This way we use Microsofts implementation
var proxy = new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.SubnetFilterAsArray);
return proxy.IsBypassed((Uri)url);
}
}
}

@ -1,40 +0,0 @@
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.Http
{
public class ProxyHttpInterceptor : IHttpRequestInterceptor
{
private readonly IConfigService _configService;
public ProxyHttpInterceptor(IConfigService configService)
{
this._configService = configService;
}
public HttpResponse PostResponse(HttpResponse response)
{
return response;
}
public HttpRequest PreRequest(HttpRequest request)
{
if(_configService.ProxyEnabled)
{
request.Proxy = new HttpRequestProxySettings(_configService.ProxyType,
_configService.ProxyHostname,
_configService.ProxyPort,
_configService.ProxySubnetFilter,
_configService.ProxyBypassLocalAddresses,
_configService.ProxyUsername,
_configService.ProxyPassword);
}
return request;
}
}
}

@ -515,7 +515,7 @@
<Compile Include="Housekeeping\HousekeepingCommand.cs" /> <Compile Include="Housekeeping\HousekeepingCommand.cs" />
<Compile Include="Housekeeping\HousekeepingService.cs" /> <Compile Include="Housekeeping\HousekeepingService.cs" />
<Compile Include="Housekeeping\IHousekeepingTask.cs" /> <Compile Include="Housekeeping\IHousekeepingTask.cs" />
<Compile Include="Http\ProxyHttpInterceptor.cs" /> <Compile Include="Http\HttpProxySettingsProvider.cs" />
<Compile Include="Http\TorcacheHttpInterceptor.cs" /> <Compile Include="Http\TorcacheHttpInterceptor.cs" />
<Compile Include="Indexers\BitMeTv\BitMeTv.cs" /> <Compile Include="Indexers\BitMeTv\BitMeTv.cs" />
<Compile Include="Indexers\BitMeTv\BitMeTvSettings.cs" /> <Compile Include="Indexers\BitMeTv\BitMeTvSettings.cs" />

@ -4,6 +4,7 @@ using Nancy.Bootstrapper;
using NzbDrone.Api; using NzbDrone.Api;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Organizer; using NzbDrone.Core.Organizer;
using NzbDrone.SignalR; using NzbDrone.SignalR;
@ -42,6 +43,7 @@ namespace NzbDrone.Host
AutoRegisterImplementations<NzbDronePersistentConnection>(); AutoRegisterImplementations<NzbDronePersistentConnection>();
Container.Register<INancyBootstrapper, NancyBootstrapper>(); Container.Register<INancyBootstrapper, NancyBootstrapper>();
Container.Register<IHttpDispatcher, FallbackHttpDispatcher>();
} }
} }
} }

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http.Dispatchers;
namespace NzbDrone.Update namespace NzbDrone.Update
{ {
@ -10,7 +11,7 @@ namespace NzbDrone.Update
private UpdateContainerBuilder(IStartupContext startupContext, string[] assemblies) private UpdateContainerBuilder(IStartupContext startupContext, string[] assemblies)
: base(startupContext, assemblies) : base(startupContext, assemblies)
{ {
Container.Register<IHttpDispatcher, FallbackHttpDispatcher>();
} }
public static IContainer Build(IStartupContext startupContext) public static IContainer Build(IStartupContext startupContext)

Loading…
Cancel
Save