Add tests for CurlHttpClient and fix the failures

pull/721/head
ta264 9 years ago
parent 9ffa28f17c
commit 4be0fe1b76

@ -218,6 +218,9 @@ Function PackageTests()
Write-Host "Adding NzbDrone.Core.dll.config (for dllmap)" Write-Host "Adding NzbDrone.Core.dll.config (for dllmap)"
Copy-Item "$sourceFolder\NzbDrone.Core\NzbDrone.Core.dll.config" -Destination $testPackageFolder -Force Copy-Item "$sourceFolder\NzbDrone.Core\NzbDrone.Core.dll.config" -Destination $testPackageFolder -Force
Write-Host "Copying CurlSharp libraries"
Copy-Item $sourceFolder\ExternalModules\CurlSharp\libs\i386\* $testPackageFolder
Write-Host "##teamcity[progressFinish 'Creating Test Package']" Write-Host "##teamcity[progressFinish 'Creating Test Package']"
} }

@ -224,6 +224,9 @@ PackageTests()
echo "Adding CurlSharp.dll.config (for dllmap)" echo "Adding CurlSharp.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder
echo "Copying CurlSharp libraries"
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
echo "##teamcity[progressFinish 'Creating Test Package']" echo "##teamcity[progressFinish 'Creating Test Package']"
} }

@ -5,3 +5,4 @@ NUNIT="$TESTDIR/NUnit.Runners.2.6.1/tools/nunit-console-x86.exe"
mono --debug --runtime=v4.0 $NUNIT $EXCLUDE -xml:NzbDrone.Api.Result.xml $TESTDIR/NzbDrone.Api.Test.dll mono --debug --runtime=v4.0 $NUNIT $EXCLUDE -xml:NzbDrone.Api.Result.xml $TESTDIR/NzbDrone.Api.Test.dll
mono --debug --runtime=v4.0 $NUNIT $EXCLUDE -xml:NzbDrone.Core.Result.xml $TESTDIR/NzbDrone.Core.Test.dll mono --debug --runtime=v4.0 $NUNIT $EXCLUDE -xml:NzbDrone.Core.Result.xml $TESTDIR/NzbDrone.Core.Test.dll
mono --debug --runtime=v4.0 $NUNIT $EXCLUDE -xml:NzbDrone.Integration.Result.xml $TESTDIR/NzbDrone.Integration.Test.dll mono --debug --runtime=v4.0 $NUNIT $EXCLUDE -xml:NzbDrone.Integration.Result.xml $TESTDIR/NzbDrone.Integration.Test.dll
mono --debug --runtime=v4.0 $NUNIT $EXCLUDE -xml:NzbDrone.Common.Result.xml $TESTDIR/NzbDrone.Common.Test.dll

@ -14,16 +14,33 @@ using Moq;
namespace NzbDrone.Common.Test.Http namespace NzbDrone.Common.Test.Http
{ {
[TestFixture] [TestFixture(true)]
[TestFixture(false)]
[IntegrationTest] [IntegrationTest]
public class HttpClientFixture : TestBase<HttpClient> public class HttpClientFixture : TestBase<HttpClient>
{ {
private bool _forceCurl;
public HttpClientFixture(bool forceCurl)
{
_forceCurl = forceCurl;
}
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>()); Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>()); Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new IHttpRequestInterceptor[0]); Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(new IHttpRequestInterceptor[0]);
if (_forceCurl)
{
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<CurlHttpDispatcher>());
}
else
{
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<ManagedHttpDispatcher>());
}
} }
[Test] [Test]
@ -35,6 +52,16 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace(); response.Content.Should().NotBeNullOrWhiteSpace();
} }
[Test]
public void should_execute_https_get()
{
var request = new HttpRequest("https://eu.httpbin.org/get");
var response = Subject.Execute(request);
response.Content.Should().NotBeNullOrWhiteSpace();
}
[Test] [Test]
public void should_execute_typed_get() public void should_execute_typed_get()
@ -163,7 +190,7 @@ namespace NzbDrone.Common.Test.Http
var oldRequest = new HttpRequest("http://eu.httpbin.org/get"); var oldRequest = new HttpRequest("http://eu.httpbin.org/get");
oldRequest.AddCookie("my", "cookie"); oldRequest.AddCookie("my", "cookie");
var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<Logger>()); var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.Resolve<Logger>());
oldClient.Should().NotBeSameAs(Subject); oldClient.Should().NotBeSameAs(Subject);
@ -295,6 +322,32 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IHttpRequestInterceptor>() Mocker.GetMock<IHttpRequestInterceptor>()
.Verify(v => v.PostResponse(It.IsAny<HttpResponse>()), Times.Once()); .Verify(v => v.PostResponse(It.IsAny<HttpResponse>()), Times.Once());
} }
public void should_parse_malformed_cloudflare_cookie()
{
// the date is bad in the below - should be 13-Jul-2016
string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Wed, 13-Jul-16 16:19:50 GMT; path=/; HttpOnly";
string url = "http://eu.httpbin.org/response-headers?Set-Cookie=" +
System.Uri.EscapeUriString(malformedCookie);
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true;
var responseSet = Subject.Get(requestSet);
var request = new HttpRequest("http://eu.httpbin.org/get");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers.Should().ContainKey("Cookie");
var cookie = response.Resource.Headers["Cookie"].ToString();
cookie.Should().Contain("__cfduid=d29e686a9d65800021c66faca0a29b4261436890790");
ExceptionVerification.IgnoreErrors();
}
} }
public class HttpBinResource public class HttpBinResource

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -142,6 +142,9 @@
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" /> <Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
<PropertyGroup>
<PostBuildEvent Condition="'$(Configuration)|$(OS)' == 'Debug|Windows_NT'">xcopy /s /y "$(SolutionDir)\ExternalModules\CurlSharp\libs\i386\*" "$(TargetDir)"</PostBuildEvent>
</PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">
@ -149,4 +152,4 @@
<Target Name="AfterBuild"> <Target Name="AfterBuild">
</Target> </Target>
--> -->
</Project> </Project>

@ -6,8 +6,10 @@ using System.Linq;
using System.Net; using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using CurlSharp; using CurlSharp;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
@ -16,6 +18,7 @@ namespace NzbDrone.Common.Http
public class CurlHttpClient public class CurlHttpClient
{ {
private static Logger Logger = NzbDroneLogger.GetLogger(typeof(CurlHttpClient)); private static Logger Logger = NzbDroneLogger.GetLogger(typeof(CurlHttpClient));
private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public CurlHttpClient() public CurlHttpClient()
{ {
@ -64,7 +67,12 @@ namespace NzbDrone.Common.Http
curlEasy.HttpGet = webRequest.Method == "GET"; curlEasy.HttpGet = webRequest.Method == "GET";
curlEasy.Post = webRequest.Method == "POST"; curlEasy.Post = webRequest.Method == "POST";
curlEasy.Put = webRequest.Method == "PUT"; curlEasy.Put = webRequest.Method == "PUT";
curlEasy.Url = webRequest.RequestUri.ToString(); curlEasy.Url = webRequest.RequestUri.AbsoluteUri;
if (OsInfo.IsWindows)
{
curlEasy.CaInfo = "curl-ca-bundle.crt";
}
if (webRequest.CookieContainer != null) if (webRequest.CookieContainer != null)
{ {
@ -152,20 +160,34 @@ namespace NzbDrone.Common.Http
var webHeaderCollection = new WebHeaderCollection(); var webHeaderCollection = new WebHeaderCollection();
foreach (var header in headerString.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1)) // following a redirect we could have two sets of headers, so only process the last one
foreach (var header in headerString.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Reverse())
{ {
if (!header.Contains(":")) break;
webHeaderCollection.Add(header); webHeaderCollection.Add(header);
} }
var setCookie = webHeaderCollection.Get("Set-Cookie"); var setCookie = webHeaderCollection.Get("Set-Cookie");
if (setCookie != null && setCookie.Length > 0 && webRequest.CookieContainer != null) if (setCookie != null && setCookie.Length > 0 && webRequest.CookieContainer != null)
{ {
webRequest.CookieContainer.SetCookies(webRequest.RequestUri, setCookie); webRequest.CookieContainer.SetCookies(webRequest.RequestUri, FixSetCookieHeader(setCookie));
} }
return webHeaderCollection; return webHeaderCollection;
} }
private string FixSetCookieHeader(string setCookie)
{
// fix up the date if it was malformed
var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match)
{
string format = "ddd, dd-MMM-yyyy HH:mm:ss";
DateTime dt = Convert.ToDateTime(match.Groups[2].Value);
return match.Groups[1].Value + dt.ToUniversalTime().ToString(format) + " GMT";
});
return setCookieClean;
}
private byte[] ProcessResponseStream(HttpWebRequest webRequest, Stream responseStream, WebHeaderCollection webHeaderCollection) private byte[] ProcessResponseStream(HttpWebRequest webRequest, Stream responseStream, WebHeaderCollection webHeaderCollection)
{ {
responseStream.Position = 0; responseStream.Position = 0;

@ -23,6 +23,119 @@ namespace NzbDrone.Common.Http
HttpResponse<T> Post<T>(HttpRequest request) where T : new(); HttpResponse<T> Post<T>(HttpRequest request) where T : new();
} }
public interface IHttpDispatcher
{
HttpResponse GetResponse(HttpRequest request, HttpWebRequest webRequest);
}
public class ManagedHttpDispatcher : IHttpDispatcher
{
public HttpResponse GetResponse(HttpRequest request, HttpWebRequest webRequest)
{
if (!request.Body.IsNullOrWhiteSpace())
{
var bytes = request.Headers.GetEncodingFromContentType().GetBytes(request.Body.ToCharArray());
webRequest.ContentLength = bytes.Length;
using (var writeStream = webRequest.GetRequestStream())
{
writeStream.Write(bytes, 0, bytes.Length);
}
}
HttpWebResponse httpWebResponse;
try
{
httpWebResponse = (HttpWebResponse)webRequest.GetResponse();
}
catch (WebException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{
throw;
}
}
Byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null)
{
data = responseStream.ToBytes();
}
}
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
}
}
public class CurlHttpDispatcher : IHttpDispatcher
{
public HttpResponse GetResponse(HttpRequest request, HttpWebRequest webRequest)
{
var curlClient = new CurlHttpClient();
return curlClient.GetResponse(request, webRequest);
}
}
public class FallbackHttpDispatcher : IHttpDispatcher
{
private readonly Logger _logger;
private readonly ICached<bool> _curlTLSFallbackCache;
public FallbackHttpDispatcher(ICached<bool> curlTLSFallbackCache, Logger logger)
{
_logger = logger;
_curlTLSFallbackCache = curlTLSFallbackCache;
}
public HttpResponse GetResponse(HttpRequest request, HttpWebRequest webRequest)
{
ManagedHttpDispatcher managedDispatcher = new ManagedHttpDispatcher();
CurlHttpDispatcher curlDispatcher = new CurlHttpDispatcher();
if (OsInfo.IsMonoRuntime && webRequest.RequestUri.Scheme == "https")
{
if (!_curlTLSFallbackCache.Find(webRequest.RequestUri.Host))
{
try
{
return managedDispatcher.GetResponse(request, webRequest);
}
catch (Exception ex)
{
if (ex.ToString().Contains("The authentication or decryption has failed."))
{
_logger.Debug("https request failed in tls error for {0}, trying curl fallback.", webRequest.RequestUri.Host);
_curlTLSFallbackCache.Set(webRequest.RequestUri.Host, true);
}
else
{
throw;
}
}
}
if (CurlHttpClient.CheckAvailability())
{
return curlDispatcher.GetResponse(request, webRequest);
}
_logger.Trace("Curl not available, using default WebClient.");
}
return managedDispatcher.GetResponse(request, webRequest);
}
}
public class HttpClient : IHttpClient public class HttpClient : IHttpClient
{ {
private readonly Logger _logger; private readonly Logger _logger;
@ -30,16 +143,23 @@ namespace NzbDrone.Common.Http
private readonly ICached<CookieContainer> _cookieContainerCache; private readonly ICached<CookieContainer> _cookieContainerCache;
private readonly ICached<bool> _curlTLSFallbackCache; private readonly ICached<bool> _curlTLSFallbackCache;
private readonly List<IHttpRequestInterceptor> _requestInterceptors; private readonly List<IHttpRequestInterceptor> _requestInterceptors;
private readonly IHttpDispatcher _httpDispatcher;
public HttpClient(IEnumerable<IHttpRequestInterceptor> requestInterceptors, ICacheManager cacheManager, IRateLimitService rateLimitService, Logger logger) public HttpClient(IEnumerable<IHttpRequestInterceptor> requestInterceptors, ICacheManager cacheManager, IRateLimitService rateLimitService, IHttpDispatcher httpDispatcher, Logger logger)
{ {
_logger = logger; _logger = logger;
_rateLimitService = rateLimitService; _rateLimitService = rateLimitService;
_requestInterceptors = requestInterceptors.ToList(); _requestInterceptors = requestInterceptors.ToList();
ServicePointManager.DefaultConnectionLimit = 12; ServicePointManager.DefaultConnectionLimit = 12;
_httpDispatcher = httpDispatcher;
_cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient)); _cookieContainerCache = cacheManager.GetCache<CookieContainer>(typeof(HttpClient));
_curlTLSFallbackCache = cacheManager.GetCache<bool>(typeof(HttpClient), "curlTLSFallback"); }
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)
@ -79,7 +199,7 @@ namespace NzbDrone.Common.Http
PrepareRequestCookies(request, webRequest); PrepareRequestCookies(request, webRequest);
var response = ExecuteRequest(request, webRequest); var response = _httpDispatcher.GetResponse(request, webRequest);
HandleResponseCookies(request, webRequest); HandleResponseCookies(request, webRequest);
@ -89,8 +209,8 @@ namespace NzbDrone.Common.Http
if (!RuntimeInfoBase.IsProduction && if (!RuntimeInfoBase.IsProduction &&
(response.StatusCode == HttpStatusCode.Moved || (response.StatusCode == HttpStatusCode.Moved ||
response.StatusCode == HttpStatusCode.MovedPermanently || response.StatusCode == HttpStatusCode.MovedPermanently ||
response.StatusCode == HttpStatusCode.Found)) response.StatusCode == HttpStatusCode.Found))
{ {
_logger.Error("Server requested a redirect to [" + response.Headers["Location"] + "]. Update the request URL to avoid this redirect."); _logger.Error("Server requested a redirect to [" + response.Headers["Location"] + "]. Update the request URL to avoid this redirect.");
} }
@ -129,7 +249,9 @@ namespace NzbDrone.Common.Http
{ {
persistentCookieContainer.Add(new Cookie(pair.Key, pair.Value, "/", request.Url.Host) persistentCookieContainer.Add(new Cookie(pair.Key, pair.Value, "/", request.Url.Host)
{ {
Expires = DateTime.UtcNow.AddHours(1) // Use Now rather than UtcNow to work around Mono cookie expiry bug.
// See https://gist.github.com/ta264/7822b1424f72e5b4c961
Expires = DateTime.Now.AddHours(1)
}); });
} }
} }
@ -167,91 +289,6 @@ namespace NzbDrone.Common.Http
} }
} }
private HttpResponse ExecuteRequest(HttpRequest request, HttpWebRequest webRequest)
{
if (OsInfo.IsMonoRuntime && webRequest.RequestUri.Scheme == "https")
{
if (!_curlTLSFallbackCache.Find(webRequest.RequestUri.Host))
{
try
{
return ExecuteWebRequest(request, webRequest);
}
catch (Exception ex)
{
if (ex.ToString().Contains("The authentication or decryption has failed."))
{
_logger.Debug("https request failed in tls error for {0}, trying curl fallback.", webRequest.RequestUri.Host);
_curlTLSFallbackCache.Set(webRequest.RequestUri.Host, true);
}
else
{
throw;
}
}
}
if (CurlHttpClient.CheckAvailability())
{
return ExecuteCurlRequest(request, webRequest);
}
_logger.Trace("Curl not available, using default WebClient.");
}
return ExecuteWebRequest(request, webRequest);
}
private HttpResponse ExecuteCurlRequest(HttpRequest request, HttpWebRequest webRequest)
{
var curlClient = new CurlHttpClient();
return curlClient.GetResponse(request, webRequest);
}
private HttpResponse ExecuteWebRequest(HttpRequest request, HttpWebRequest webRequest)
{
if (!request.Body.IsNullOrWhiteSpace())
{
var bytes = request.Headers.GetEncodingFromContentType().GetBytes(request.Body.ToCharArray());
webRequest.ContentLength = bytes.Length;
using (var writeStream = webRequest.GetRequestStream())
{
writeStream.Write(bytes, 0, bytes.Length);
}
}
HttpWebResponse httpWebResponse;
try
{
httpWebResponse = (HttpWebResponse)webRequest.GetResponse();
}
catch (WebException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{
throw;
}
}
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
{
if (responseStream != null)
{
data = responseStream.ToBytes();
}
}
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
}
public void DownloadFile(string url, string fileName) public void DownloadFile(string url, string fileName)
{ {
try try

@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
{ {
public class HttpHeader : Dictionary<string, object> public class HttpHeader : Dictionary<string, object>
{ {
public HttpHeader(NameValueCollection headers) public HttpHeader(NameValueCollection headers) : base(StringComparer.OrdinalIgnoreCase)
{ {
foreach (var key in headers.AllKeys) foreach (var key in headers.AllKeys)
{ {
@ -17,7 +17,7 @@ namespace NzbDrone.Common.Http
} }
} }
public HttpHeader() public HttpHeader() : base(StringComparer.OrdinalIgnoreCase)
{ {
} }

Loading…
Cancel
Save