|
|
@ -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
|
|
|
|