Refactored the HttpDispatchers.
This commit is contained in:
parent
e13c89521d
commit
fe76d0f98f
|
@ -11,36 +11,22 @@ using NzbDrone.Test.Common.Categories;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.TPL;
|
using NzbDrone.Common.TPL;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using NzbDrone.Common.Http.Dispatchers;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Test.Http
|
namespace NzbDrone.Common.Test.Http
|
||||||
{
|
{
|
||||||
[TestFixture(true)]
|
|
||||||
[TestFixture(false)]
|
|
||||||
[IntegrationTest]
|
[IntegrationTest]
|
||||||
public class HttpClientFixture : TestBase<HttpClient>
|
[TestFixture(typeof(ManagedHttpDispatcher))]
|
||||||
{
|
[TestFixture(typeof(CurlHttpDispatcher))]
|
||||||
private bool _forceCurl;
|
public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher
|
||||||
|
{
|
||||||
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]);
|
||||||
|
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
|
||||||
if (_forceCurl)
|
|
||||||
{
|
|
||||||
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<CurlHttpDispatcher>());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<ManagedHttpDispatcher>());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -13,20 +12,13 @@ using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Instrumentation;
|
using NzbDrone.Common.Instrumentation;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http.Dispatchers
|
||||||
{
|
{
|
||||||
public class CurlHttpClient
|
public class CurlHttpDispatcher : IHttpDispatcher
|
||||||
{
|
{
|
||||||
private static Logger Logger = NzbDroneLogger.GetLogger(typeof(CurlHttpClient));
|
|
||||||
private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
public CurlHttpClient()
|
private static readonly Logger _logger = NzbDroneLogger.GetLogger(typeof(CurlHttpDispatcher));
|
||||||
{
|
|
||||||
if (!CheckAvailability())
|
|
||||||
{
|
|
||||||
throw new ApplicationException("Curl failed to initialize.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool CheckAvailability()
|
public static bool CheckAvailability()
|
||||||
{
|
{
|
||||||
|
@ -36,13 +28,23 @@ namespace NzbDrone.Common.Http
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.TraceException("Initializing curl failed", ex);
|
_logger.TraceException("Initializing curl failed", ex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponse GetResponse(HttpRequest httpRequest, HttpWebRequest webRequest)
|
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
||||||
{
|
{
|
||||||
|
if (!CheckAvailability())
|
||||||
|
{
|
||||||
|
throw new ApplicationException("Curl failed to initialize.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.NetworkCredential != null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("Credentials not supported for curl dispatcher.");
|
||||||
|
}
|
||||||
|
|
||||||
lock (CurlGlobalHandle.Instance)
|
lock (CurlGlobalHandle.Instance)
|
||||||
{
|
{
|
||||||
Stream responseStream = new MemoryStream();
|
Stream responseStream = new MemoryStream();
|
||||||
|
@ -61,33 +63,47 @@ namespace NzbDrone.Common.Http
|
||||||
headerStream.Write(b, 0, s * n);
|
headerStream.Write(b, 0, s * n);
|
||||||
return s * n;
|
return s * n;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
curlEasy.Url = request.Url.AbsoluteUri;
|
||||||
|
switch (request.Method)
|
||||||
|
{
|
||||||
|
case HttpMethod.GET:
|
||||||
|
curlEasy.HttpGet = true;
|
||||||
|
break;
|
||||||
|
|
||||||
curlEasy.UserAgent = webRequest.UserAgent;
|
case HttpMethod.POST:
|
||||||
curlEasy.FollowLocation = webRequest.AllowAutoRedirect;
|
curlEasy.Post = true;
|
||||||
curlEasy.HttpGet = webRequest.Method == "GET";
|
break;
|
||||||
curlEasy.Post = webRequest.Method == "POST";
|
|
||||||
curlEasy.Put = webRequest.Method == "PUT";
|
case HttpMethod.PUT:
|
||||||
curlEasy.Url = webRequest.RequestUri.AbsoluteUri;
|
curlEasy.Put = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException(string.Format("HttpCurl method {0} not supported", request.Method));
|
||||||
|
}
|
||||||
|
curlEasy.UserAgent = UserAgentBuilder.UserAgent;
|
||||||
|
curlEasy.FollowLocation = request.AllowAutoRedirect;
|
||||||
|
|
||||||
if (OsInfo.IsWindows)
|
if (OsInfo.IsWindows)
|
||||||
{
|
{
|
||||||
curlEasy.CaInfo = "curl-ca-bundle.crt";
|
curlEasy.CaInfo = "curl-ca-bundle.crt";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webRequest.CookieContainer != null)
|
if (cookies != null)
|
||||||
{
|
{
|
||||||
curlEasy.Cookie = webRequest.CookieContainer.GetCookieHeader(webRequest.RequestUri);
|
curlEasy.Cookie = cookies.GetCookieHeader(request.Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!httpRequest.Body.IsNullOrWhiteSpace())
|
if (!request.Body.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
// TODO: This might not go well with encoding.
|
// TODO: This might not go well with encoding.
|
||||||
curlEasy.PostFieldSize = httpRequest.Body.Length;
|
curlEasy.PostFieldSize = request.Body.Length;
|
||||||
curlEasy.SetOpt(CurlOption.CopyPostFields, httpRequest.Body);
|
curlEasy.SetOpt(CurlOption.CopyPostFields, request.Body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state
|
// Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state
|
||||||
using (var httpRequestHeaders = SerializeHeaders(webRequest))
|
using (var httpRequestHeaders = SerializeHeaders(request))
|
||||||
{
|
{
|
||||||
curlEasy.HttpHeader = httpRequestHeaders;
|
curlEasy.HttpHeader = httpRequestHeaders;
|
||||||
|
|
||||||
|
@ -99,60 +115,38 @@ namespace NzbDrone.Common.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var webHeaderCollection = ProcessHeaderStream(webRequest, headerStream);
|
var webHeaderCollection = ProcessHeaderStream(request, cookies, headerStream);
|
||||||
var responseData = ProcessResponseStream(webRequest, responseStream, webHeaderCollection);
|
var responseData = ProcessResponseStream(request, responseStream, webHeaderCollection);
|
||||||
|
|
||||||
var httpHeader = new HttpHeader(webHeaderCollection);
|
var httpHeader = new HttpHeader(webHeaderCollection);
|
||||||
|
|
||||||
return new HttpResponse(httpRequest, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode);
|
return new HttpResponse(request, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CurlSlist SerializeHeaders(HttpWebRequest webRequest)
|
private CurlSlist SerializeHeaders(HttpRequest request)
|
||||||
{
|
{
|
||||||
if (webRequest.SendChunked)
|
if (!request.Headers.ContainsKey("Accept-Encoding"))
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Chunked transfer is not supported");
|
request.Headers.Add("Accept-Encoding", "gzip");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webRequest.ContentLength > 0)
|
if (request.Headers.ContentType == null)
|
||||||
{
|
{
|
||||||
webRequest.Headers.Add("Content-Length", webRequest.ContentLength.ToString());
|
request.Headers.ContentType = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.GZip))
|
|
||||||
{
|
|
||||||
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.Deflate))
|
|
||||||
{
|
|
||||||
webRequest.Headers.Add("Accept-Encoding", "gzip, deflate");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
webRequest.Headers.Add("Accept-Encoding", "gzip");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.Deflate))
|
|
||||||
{
|
|
||||||
webRequest.Headers.Add("Accept-Encoding", "deflate");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var curlHeaders = new CurlSlist();
|
var curlHeaders = new CurlSlist();
|
||||||
for (int i = 0; i < webRequest.Headers.Count; i++)
|
foreach (var header in request.Headers)
|
||||||
{
|
{
|
||||||
curlHeaders.Append(webRequest.Headers.GetKey(i) + ": " + webRequest.Headers.Get(i));
|
curlHeaders.Append(header.Key + ": " + header.Value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
curlHeaders.Append("Content-Type: " + webRequest.ContentType ?? string.Empty);
|
|
||||||
|
|
||||||
return curlHeaders;
|
return curlHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
private WebHeaderCollection ProcessHeaderStream(HttpWebRequest webRequest, Stream headerStream)
|
private WebHeaderCollection ProcessHeaderStream(HttpRequest request, CookieContainer cookies, Stream headerStream)
|
||||||
{
|
{
|
||||||
headerStream.Position = 0;
|
headerStream.Position = 0;
|
||||||
var headerData = headerStream.ToBytes();
|
var headerData = headerStream.ToBytes();
|
||||||
|
@ -168,9 +162,9 @@ namespace NzbDrone.Common.Http
|
||||||
}
|
}
|
||||||
|
|
||||||
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 && cookies != null)
|
||||||
{
|
{
|
||||||
webRequest.CookieContainer.SetCookies(webRequest.RequestUri, FixSetCookieHeader(setCookie));
|
cookies.SetCookies(request.Url, FixSetCookieHeader(setCookie));
|
||||||
}
|
}
|
||||||
|
|
||||||
return webHeaderCollection;
|
return webHeaderCollection;
|
||||||
|
@ -180,30 +174,30 @@ namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
// fix up the date if it was malformed
|
// fix up the date if it was malformed
|
||||||
var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match)
|
var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match)
|
||||||
{
|
{
|
||||||
string format = "ddd, dd-MMM-yyyy HH:mm:ss";
|
string format = "ddd, dd-MMM-yyyy HH:mm:ss";
|
||||||
DateTime dt = Convert.ToDateTime(match.Groups[2].Value);
|
DateTime dt = Convert.ToDateTime(match.Groups[2].Value);
|
||||||
return match.Groups[1].Value + dt.ToUniversalTime().ToString(format) + " GMT";
|
return match.Groups[1].Value + dt.ToUniversalTime().ToString(format) + " GMT";
|
||||||
});
|
});
|
||||||
return setCookieClean;
|
return setCookieClean;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] ProcessResponseStream(HttpWebRequest webRequest, Stream responseStream, WebHeaderCollection webHeaderCollection)
|
private byte[] ProcessResponseStream(HttpRequest request, Stream responseStream, WebHeaderCollection webHeaderCollection)
|
||||||
{
|
{
|
||||||
responseStream.Position = 0;
|
responseStream.Position = 0;
|
||||||
|
|
||||||
if (responseStream.Length != 0 && webRequest.AutomaticDecompression != DecompressionMethods.None)
|
if (responseStream.Length != 0)
|
||||||
{
|
{
|
||||||
var encoding = webHeaderCollection["Content-Encoding"];
|
var encoding = webHeaderCollection["Content-Encoding"];
|
||||||
if (encoding != null)
|
if (encoding != null)
|
||||||
{
|
{
|
||||||
if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.GZip) && encoding.IndexOf("gzip") != -1)
|
if (encoding.IndexOf("gzip") != -1)
|
||||||
{
|
{
|
||||||
responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
|
responseStream = new GZipStream(responseStream, CompressionMode.Decompress);
|
||||||
|
|
||||||
webHeaderCollection.Remove("Content-Encoding");
|
webHeaderCollection.Remove("Content-Encoding");
|
||||||
}
|
}
|
||||||
else if (webRequest.AutomaticDecompression.HasFlag(DecompressionMethods.Deflate) && encoding.IndexOf("deflate") != -1)
|
else if (encoding.IndexOf("deflate") != -1)
|
||||||
{
|
{
|
||||||
responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);
|
responseStream = new DeflateStream(responseStream, CompressionMode.Decompress);
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Http.Dispatchers
|
||||||
|
{
|
||||||
|
public class FallbackHttpDispatcher : IHttpDispatcher
|
||||||
|
{
|
||||||
|
private readonly Logger _logger;
|
||||||
|
private readonly ICached<bool> _curlTLSFallbackCache;
|
||||||
|
private readonly ManagedHttpDispatcher _managedDispatcher;
|
||||||
|
private readonly CurlHttpDispatcher _curlDispatcher;
|
||||||
|
|
||||||
|
public FallbackHttpDispatcher(ICached<bool> curlTLSFallbackCache, Logger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_curlTLSFallbackCache = curlTLSFallbackCache;
|
||||||
|
_managedDispatcher = new ManagedHttpDispatcher();
|
||||||
|
_curlDispatcher = new CurlHttpDispatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
||||||
|
{
|
||||||
|
if (OsInfo.IsMonoRuntime && request.Url.Scheme == "https")
|
||||||
|
{
|
||||||
|
if (!_curlTLSFallbackCache.Find(request.Url.Host))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _managedDispatcher.GetResponse(request, cookies);
|
||||||
|
}
|
||||||
|
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.", request.Url.Host);
|
||||||
|
|
||||||
|
_curlTLSFallbackCache.Set(request.Url.Host, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CurlHttpDispatcher.CheckAvailability())
|
||||||
|
{
|
||||||
|
return _curlDispatcher.GetResponse(request, cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Curl not available, using default WebClient.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _managedDispatcher.GetResponse(request, cookies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.TPL;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Http.Dispatchers
|
||||||
|
{
|
||||||
|
public interface IHttpDispatcher
|
||||||
|
{
|
||||||
|
HttpResponse GetResponse(HttpRequest request, CookieContainer cookies);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Http.Dispatchers
|
||||||
|
{
|
||||||
|
public class ManagedHttpDispatcher : IHttpDispatcher
|
||||||
|
{
|
||||||
|
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
|
||||||
|
{
|
||||||
|
var webRequest = (HttpWebRequest)WebRequest.Create(request.Url);
|
||||||
|
|
||||||
|
// Deflate is not a standard and could break depending on implementation.
|
||||||
|
// we should just stick with the more compatible Gzip
|
||||||
|
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
|
||||||
|
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||||
|
|
||||||
|
webRequest.Credentials = request.NetworkCredential;
|
||||||
|
webRequest.Method = request.Method.ToString();
|
||||||
|
webRequest.UserAgent = UserAgentBuilder.UserAgent;
|
||||||
|
webRequest.KeepAlive = false;
|
||||||
|
webRequest.AllowAutoRedirect = request.AllowAutoRedirect;
|
||||||
|
webRequest.ContentLength = 0;
|
||||||
|
webRequest.CookieContainer = cookies;
|
||||||
|
|
||||||
|
if (request.Headers != null)
|
||||||
|
{
|
||||||
|
AddRequestHeaders(webRequest, request.Headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers)
|
||||||
|
{
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
switch (header.Key)
|
||||||
|
{
|
||||||
|
case "Accept":
|
||||||
|
webRequest.Accept = header.Value.ToString();
|
||||||
|
break;
|
||||||
|
case "Connection":
|
||||||
|
webRequest.Connection = header.Value.ToString();
|
||||||
|
break;
|
||||||
|
case "Content-Length":
|
||||||
|
webRequest.ContentLength = Convert.ToInt64(header.Value);
|
||||||
|
break;
|
||||||
|
case "Content-Type":
|
||||||
|
webRequest.ContentType = header.Value.ToString();
|
||||||
|
break;
|
||||||
|
case "Date":
|
||||||
|
webRequest.Date = (DateTime)header.Value;
|
||||||
|
break;
|
||||||
|
case "Expect":
|
||||||
|
webRequest.Expect = header.Value.ToString();
|
||||||
|
break;
|
||||||
|
case "Host":
|
||||||
|
webRequest.Host = header.Value.ToString();
|
||||||
|
break;
|
||||||
|
case "If-Modified-Since":
|
||||||
|
webRequest.IfModifiedSince = (DateTime)header.Value;
|
||||||
|
break;
|
||||||
|
case "Range":
|
||||||
|
throw new NotImplementedException();
|
||||||
|
break;
|
||||||
|
case "Referer":
|
||||||
|
webRequest.Referer = header.Value.ToString();
|
||||||
|
break;
|
||||||
|
case "Transfer-Encoding":
|
||||||
|
webRequest.TransferEncoding = header.Value.ToString();
|
||||||
|
break;
|
||||||
|
case "User-Agent":
|
||||||
|
throw new NotSupportedException("User-Agent other than Sonarr not allowed.");
|
||||||
|
case "Proxy-Connection":
|
||||||
|
throw new NotImplementedException();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
webRequest.Headers.Add(header.Key, header.Value.ToString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http.Dispatchers;
|
||||||
using NzbDrone.Common.TPL;
|
using NzbDrone.Common.TPL;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
|
@ -23,119 +24,6 @@ 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;
|
||||||
|
@ -176,37 +64,23 @@ namespace NzbDrone.Common.Http
|
||||||
|
|
||||||
_logger.Trace(request);
|
_logger.Trace(request);
|
||||||
|
|
||||||
var webRequest = (HttpWebRequest)WebRequest.Create(request.Url);
|
|
||||||
|
|
||||||
// Deflate is not a standard and could break depending on implementation.
|
|
||||||
// we should just stick with the more compatible Gzip
|
|
||||||
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
|
|
||||||
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
|
||||||
|
|
||||||
webRequest.Credentials = request.NetworkCredential;
|
|
||||||
webRequest.Method = request.Method.ToString();
|
|
||||||
webRequest.UserAgent = UserAgentBuilder.UserAgent;
|
|
||||||
webRequest.KeepAlive = false;
|
|
||||||
webRequest.AllowAutoRedirect = request.AllowAutoRedirect;
|
|
||||||
webRequest.ContentLength = 0;
|
|
||||||
|
|
||||||
var stopWatch = Stopwatch.StartNew();
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
if (request.Headers != null)
|
var cookies = PrepareRequestCookies(request);
|
||||||
{
|
|
||||||
AddRequestHeaders(webRequest, request.Headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
PrepareRequestCookies(request, webRequest);
|
var response = _httpDispatcher.GetResponse(request, cookies);
|
||||||
|
|
||||||
var response = _httpDispatcher.GetResponse(request, webRequest);
|
HandleResponseCookies(request, cookies);
|
||||||
|
|
||||||
HandleResponseCookies(request, webRequest);
|
|
||||||
|
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
|
|
||||||
_logger.Trace("{0} ({1:n0} ms)", response, stopWatch.ElapsedMilliseconds);
|
_logger.Trace("{0} ({1:n0} ms)", response, stopWatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
foreach (var interceptor in _requestInterceptors)
|
||||||
|
{
|
||||||
|
response = interceptor.PostResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
if (!RuntimeInfoBase.IsProduction &&
|
if (!RuntimeInfoBase.IsProduction &&
|
||||||
(response.StatusCode == HttpStatusCode.Moved ||
|
(response.StatusCode == HttpStatusCode.Moved ||
|
||||||
response.StatusCode == HttpStatusCode.MovedPermanently ||
|
response.StatusCode == HttpStatusCode.MovedPermanently ||
|
||||||
|
@ -229,15 +103,10 @@ namespace NzbDrone.Common.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var interceptor in _requestInterceptors)
|
|
||||||
{
|
|
||||||
response = interceptor.PostResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrepareRequestCookies(HttpRequest request, HttpWebRequest webRequest)
|
private CookieContainer PrepareRequestCookies(HttpRequest request)
|
||||||
{
|
{
|
||||||
lock (_cookieContainerCache)
|
lock (_cookieContainerCache)
|
||||||
{
|
{
|
||||||
|
@ -258,21 +127,15 @@ namespace NzbDrone.Common.Http
|
||||||
|
|
||||||
var requestCookies = persistentCookieContainer.GetCookies(request.Url);
|
var requestCookies = persistentCookieContainer.GetCookies(request.Url);
|
||||||
|
|
||||||
if (requestCookies.Count == 0 && !request.StoreResponseCookie)
|
var cookieContainer = new CookieContainer();
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webRequest.CookieContainer == null)
|
cookieContainer.Add(requestCookies);
|
||||||
{
|
|
||||||
webRequest.CookieContainer = new CookieContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
webRequest.CookieContainer.Add(requestCookies);
|
return cookieContainer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleResponseCookies(HttpRequest request, HttpWebRequest webRequest)
|
private void HandleResponseCookies(HttpRequest request, CookieContainer cookieContainer)
|
||||||
{
|
{
|
||||||
if (!request.StoreResponseCookie)
|
if (!request.StoreResponseCookie)
|
||||||
{
|
{
|
||||||
|
@ -283,7 +146,7 @@ namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
|
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
|
||||||
|
|
||||||
var cookies = webRequest.CookieContainer.GetCookies(request.Url);
|
var cookies = cookieContainer.GetCookies(request.Url);
|
||||||
|
|
||||||
persistentCookieContainer.Add(cookies);
|
persistentCookieContainer.Add(cookies);
|
||||||
}
|
}
|
||||||
|
@ -349,56 +212,5 @@ namespace NzbDrone.Common.Http
|
||||||
var response = Post(request);
|
var response = Post(request);
|
||||||
return new HttpResponse<T>(response);
|
return new HttpResponse<T>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers)
|
|
||||||
{
|
|
||||||
foreach (var header in headers)
|
|
||||||
{
|
|
||||||
switch (header.Key)
|
|
||||||
{
|
|
||||||
case "Accept":
|
|
||||||
webRequest.Accept = header.Value.ToString();
|
|
||||||
break;
|
|
||||||
case "Connection":
|
|
||||||
webRequest.Connection = header.Value.ToString();
|
|
||||||
break;
|
|
||||||
case "Content-Length":
|
|
||||||
webRequest.ContentLength = Convert.ToInt64(header.Value);
|
|
||||||
break;
|
|
||||||
case "Content-Type":
|
|
||||||
webRequest.ContentType = header.Value.ToString();
|
|
||||||
break;
|
|
||||||
case "Date":
|
|
||||||
webRequest.Date = (DateTime)header.Value;
|
|
||||||
break;
|
|
||||||
case "Expect":
|
|
||||||
webRequest.Expect = header.Value.ToString();
|
|
||||||
break;
|
|
||||||
case "Host":
|
|
||||||
webRequest.Host = header.Value.ToString();
|
|
||||||
break;
|
|
||||||
case "If-Modified-Since":
|
|
||||||
webRequest.IfModifiedSince = (DateTime)header.Value;
|
|
||||||
break;
|
|
||||||
case "Range":
|
|
||||||
throw new NotImplementedException();
|
|
||||||
break;
|
|
||||||
case "Referer":
|
|
||||||
webRequest.Referer = header.Value.ToString();
|
|
||||||
break;
|
|
||||||
case "Transfer-Encoding":
|
|
||||||
webRequest.TransferEncoding = header.Value.ToString();
|
|
||||||
break;
|
|
||||||
case "User-Agent":
|
|
||||||
throw new NotSupportedException("User-Agent other than Sonarr not allowed.");
|
|
||||||
case "Proxy-Connection":
|
|
||||||
throw new NotImplementedException();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
webRequest.Headers.Add(header.Key, header.Value.ToString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -141,7 +141,10 @@
|
||||||
<Compile Include="Extensions\UrlExtensions.cs" />
|
<Compile Include="Extensions\UrlExtensions.cs" />
|
||||||
<Compile Include="Extensions\XmlExtentions.cs" />
|
<Compile Include="Extensions\XmlExtentions.cs" />
|
||||||
<Compile Include="HashUtil.cs" />
|
<Compile Include="HashUtil.cs" />
|
||||||
<Compile Include="Http\CurlHttpClient.cs" />
|
<Compile Include="Http\Dispatchers\CurlHttpDispatcher.cs" />
|
||||||
|
<Compile Include="Http\Dispatchers\FallbackHttpDispatcher.cs" />
|
||||||
|
<Compile Include="Http\Dispatchers\IHttpDispatcher.cs" />
|
||||||
|
<Compile Include="Http\Dispatchers\ManagedHttpDispatcher.cs" />
|
||||||
<Compile Include="Http\GZipWebClient.cs">
|
<Compile Include="Http\GZipWebClient.cs">
|
||||||
<SubType>Component</SubType>
|
<SubType>Component</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|
Loading…
Reference in New Issue