From 2ffbbb0e713cbc3634d45a9adcff0ec139199bf2 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 28 Feb 2016 16:41:22 +0100 Subject: [PATCH] Refactored HttpRequest and HttpRequestBuilder, moving most of the logic to the HttpRequestBuilder. Added ContentSummary to be able to describe the ContentData in a human readable form. (Useful for JsonRpc and FormData). --- .../Http/HttpClientFixture.cs | 35 ++- .../Http/HttpRequestBuilderFixture.cs | 25 ++- .../Http/HttpRequestFixture.cs | 17 -- src/NzbDrone.Common/Cloud/CloudClient.cs | 19 -- .../Cloud/SonarrCloudRequestBuilder.cs | 28 +++ .../Extensions/DictionaryExtensions.cs | 2 +- .../Http/Dispatchers/CurlHttpDispatcher.cs | 12 +- .../Http/Dispatchers/ManagedHttpDispatcher.cs | 33 +-- src/NzbDrone.Common/Http/HttpClient.cs | 2 + src/NzbDrone.Common/Http/HttpException.cs | 2 +- src/NzbDrone.Common/Http/HttpFormData.cs | 14 ++ src/NzbDrone.Common/Http/HttpHeader.cs | 125 ++++++++--- src/NzbDrone.Common/Http/HttpRequest.cs | 68 ++---- .../Http/HttpRequestBuilder.cs | 209 +++++++++++++++++- .../Http/HttpRequestBuilderFactory.cs | 36 +++ src/NzbDrone.Common/Http/HttpResponse.cs | 23 +- .../Http/JsonRpcRequestBuilder.cs | 85 +++++-- src/NzbDrone.Common/NzbDrone.Common.csproj | 4 +- .../Blackhole/TorrentBlackholeFixture.cs | 4 +- .../Blackhole/UsenetBlackholeFixture.cs | 4 +- .../UTorrentTests/UTorrentFixture.cs | 2 +- src/NzbDrone.Core.Test/FluentTest.cs | 84 ------- src/NzbDrone.Core.Test/Framework/CoreTest.cs | 2 +- .../DailySeries/DailySeriesDataProxy.cs | 11 +- .../Scene/SceneMappingProxy.cs | 11 +- .../DataAugmentation/Xem/XemProxy.cs | 38 ++-- .../Download/TorrentClientBase.cs | 2 +- src/NzbDrone.Core/Fluent.cs | 5 - .../Http/TorcacheHttpInterceptor.cs | 4 +- .../BitMeTv/BitMeTvRequestGenerator.cs | 5 +- .../BroadcastheNetRequestGenerator.cs | 9 +- .../Indexers/HDBits/HDBitsRequestGenerator.cs | 7 +- src/NzbDrone.Core/Indexers/HttpIndexerBase.cs | 2 +- .../Indexers/Newznab/NewznabRssParser.cs | 2 +- .../Indexers/Rarbg/RarbgRequestGenerator.cs | 22 +- src/NzbDrone.Core/Indexers/RssParser.cs | 10 +- .../TorrentRssIndexerRequestGenerator.cs | 5 +- .../Indexers/Torznab/TorznabRssParser.cs | 2 +- .../MetadataSource/SkyHook/SkyHookProxy.cs | 23 +- .../MediaBrowser/MediaBrowserProxy.cs | 9 +- .../Update/UpdatePackageProvider.cs | 26 ++- 41 files changed, 682 insertions(+), 346 deletions(-) delete mode 100644 src/NzbDrone.Common/Cloud/CloudClient.cs create mode 100644 src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs create mode 100644 src/NzbDrone.Common/Http/HttpFormData.cs create mode 100644 src/NzbDrone.Common/Http/HttpRequestBuilderFactory.cs diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index 2f4e3abf3..60c23b904 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -58,18 +58,20 @@ namespace NzbDrone.Common.Test.Http var response = Subject.Get(request); - response.Resource.Url.Should().Be(request.Url.ToString()); + response.Resource.Url.Should().Be(request.Url.AbsoluteUri); } [Test] public void should_execute_simple_post() { + var message = "{ my: 1 }"; + var request = new HttpRequest("http://eu.httpbin.org/post"); - request.Body = "{ my: 1 }"; + request.SetContent(message); var response = Subject.Post(request); - response.Resource.Data.Should().Be(request.Body); + response.Resource.Data.Should().Be(message); } [TestCase("gzip")] @@ -162,7 +164,7 @@ namespace NzbDrone.Common.Test.Http public void should_send_cookie() { var request = new HttpRequest("http://eu.httpbin.org/get"); - request.AddCookie("my", "cookie"); + request.Cookies["my"] = "cookie"; var response = Subject.Get(request); @@ -176,7 +178,7 @@ namespace NzbDrone.Common.Test.Http public void GivenOldCookie() { var oldRequest = new HttpRequest("http://eu.httpbin.org/get"); - oldRequest.AddCookie("my", "cookie"); + oldRequest.Cookies["my"] = "cookie"; var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve()); @@ -260,7 +262,7 @@ namespace NzbDrone.Common.Test.Http var requestSet = new HttpRequest("http://eu.httpbin.org/cookies/set?my=cookie"); requestSet.AllowAutoRedirect = false; requestSet.StoreResponseCookie = true; - requestSet.AddCookie("my", "oldcookie"); + requestSet.Cookies["my"] = "oldcookie"; var responseSet = Subject.Get(requestSet); @@ -322,10 +324,10 @@ namespace NzbDrone.Common.Test.Http { // 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 HttpRequestBuilder("http://eu.httpbin.org/response-headers") + .AddQueryParam("Set-Cookie", malformedCookie) + .Build(); - var requestSet = new HttpRequest(url); requestSet.AllowAutoRedirect = false; requestSet.StoreResponseCookie = true; @@ -376,6 +378,21 @@ namespace NzbDrone.Common.Test.Http { } } + + public void should_submit_formparameters_in_body() + { + Assert.Fail(); + } + + public void should_submit_attachments_as_multipart() + { + Assert.Fail(); + } + + public void should_submit_formparameters_as_multipart_if_attachments_exist() + { + Assert.Fail(); + } } public class HttpBinResource diff --git a/src/NzbDrone.Common.Test/Http/HttpRequestBuilderFixture.cs b/src/NzbDrone.Common.Test/Http/HttpRequestBuilderFixture.cs index 3c4373bf0..9a1faf535 100644 --- a/src/NzbDrone.Common.Test/Http/HttpRequestBuilderFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpRequestBuilderFixture.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using System; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Common.Http; using NzbDrone.Test.Common; @@ -8,14 +9,32 @@ namespace NzbDrone.Common.Test.Http [TestFixture] public class HttpRequestBuilderFixture : TestBase { + [TestCase("http://host/{seg}/some", "http://host/dir/some")] + [TestCase("http://host/some/{seg}", "http://host/some/dir")] + public void should_add_single_segment_url_segments(string url, string result) + { + var requestBuilder = new HttpRequestBuilder(url); + + requestBuilder.SetSegment("seg", "dir"); + + requestBuilder.Build().Url.Should().Be(result); + } + + [Test] + public void shouldnt_add_value_for_nonexisting_segment() + { + var requestBuilder = new HttpRequestBuilder("http://host/{seg}/some"); + Assert.Throws(() => requestBuilder.SetSegment("seg2", "dir")); + } + [Test] public void should_remove_duplicated_slashes() { var builder = new HttpRequestBuilder("http://domain/"); - var request = builder.Build("/v1/"); + var request = builder.Resource("/v1/").Build(); - request.Url.ToString().Should().Be("http://domain/v1/"); + request.Url.AbsoluteUri.Should().Be("http://domain/v1/"); } } diff --git a/src/NzbDrone.Common.Test/Http/HttpRequestFixture.cs b/src/NzbDrone.Common.Test/Http/HttpRequestFixture.cs index 9ec350c05..57600b3e5 100644 --- a/src/NzbDrone.Common.Test/Http/HttpRequestFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpRequestFixture.cs @@ -8,22 +8,5 @@ namespace NzbDrone.Common.Test.Http [TestFixture] public class HttpRequestFixture { - [TestCase("http://host/{seg}/some", "http://host/dir/some")] - [TestCase("http://host/some/{seg}", "http://host/some/dir")] - public void should_add_single_segment_url_segments(string url, string result) - { - var request = new HttpRequest(url); - - request.AddSegment("seg", "dir"); - - request.Url.Should().Be(result); - } - - [Test] - public void shouldnt_add_value_for_nonexisting_segment() - { - var request = new HttpRequest("http://host/{seg}/some"); - Assert.Throws(() => request.AddSegment("seg2", "dir")); - } } } \ No newline at end of file diff --git a/src/NzbDrone.Common/Cloud/CloudClient.cs b/src/NzbDrone.Common/Cloud/CloudClient.cs deleted file mode 100644 index 04c3b7b8a..000000000 --- a/src/NzbDrone.Common/Cloud/CloudClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using NzbDrone.Common.Http; - -namespace NzbDrone.Common.Cloud -{ - public interface IDroneServicesRequestBuilder - { - HttpRequest Build(string path); - } - - public class DroneServicesHttpRequestBuilder : HttpRequestBuilder, IDroneServicesRequestBuilder - { - private const string ROOT_URL = "http://services.sonarr.tv/v1/"; - - public DroneServicesHttpRequestBuilder() - : base(ROOT_URL) - { - } - } -} diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs new file mode 100644 index 000000000..6afaefebb --- /dev/null +++ b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs @@ -0,0 +1,28 @@ +using System; +using NzbDrone.Common.Http; + +namespace NzbDrone.Common.Cloud +{ + public interface ISonarrCloudRequestBuilder + { + IHttpRequestBuilderFactory Services { get; } + IHttpRequestBuilderFactory SkyHookTvdb { get; } + } + + public class SonarrCloudRequestBuilder : ISonarrCloudRequestBuilder + { + public SonarrCloudRequestBuilder() + { + Services = new HttpRequestBuilder("http://services.sonarr.tv/v1/") + .CreateFactory(); + + SkyHookTvdb = new HttpRequestBuilder("http://skyhook.sonarr.tv/v1/tvdb/{route}/{language}/") + .SetSegment("language", "en") + .CreateFactory(); + } + + public IHttpRequestBuilderFactory Services { get; private set; } + + public IHttpRequestBuilderFactory SkyHookTvdb { get; private set; } + } +} diff --git a/src/NzbDrone.Common/Extensions/DictionaryExtensions.cs b/src/NzbDrone.Common/Extensions/DictionaryExtensions.cs index 7dbddb436..d14452172 100644 --- a/src/NzbDrone.Common/Extensions/DictionaryExtensions.cs +++ b/src/NzbDrone.Common/Extensions/DictionaryExtensions.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Common.Extensions public static void Add(this ICollection> collection, TKey key, TValue value) { - collection.Add(key, value); + collection.Add(new KeyValuePair(key, value)); } } } diff --git a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs index 8346ac4d7..33c8ab0b3 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs @@ -86,6 +86,11 @@ namespace NzbDrone.Common.Http.Dispatchers curlEasy.UserAgent = UserAgentBuilder.UserAgent; curlEasy.FollowLocation = request.AllowAutoRedirect; + if (request.RequestTimeout != TimeSpan.Zero) + { + curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds); + } + if (OsInfo.IsWindows) { curlEasy.CaInfo = "curl-ca-bundle.crt"; @@ -96,11 +101,10 @@ namespace NzbDrone.Common.Http.Dispatchers curlEasy.Cookie = cookies.GetCookieHeader(request.Url); } - if (!request.Body.IsNullOrWhiteSpace()) + if (request.ContentData != null) { - // TODO: This might not go well with encoding. - curlEasy.PostFieldSize = request.Body.Length; - curlEasy.SetOpt(CurlOption.CopyPostFields, request.Body); + curlEasy.PostFieldSize = request.ContentData.Length; + curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v))); } // Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 1a57f345d..4aa83d43b 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -23,19 +23,22 @@ namespace NzbDrone.Common.Http.Dispatchers webRequest.ContentLength = 0; webRequest.CookieContainer = cookies; + if (request.RequestTimeout != TimeSpan.Zero) + { + webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); + } + if (request.Headers != null) { AddRequestHeaders(webRequest, request.Headers); } - if (!request.Body.IsNullOrWhiteSpace()) + if (request.ContentData != null) { - var bytes = request.Headers.GetEncodingFromContentType().GetBytes(request.Body.ToCharArray()); - - webRequest.ContentLength = bytes.Length; + webRequest.ContentLength = request.ContentData.Length; using (var writeStream = webRequest.GetRequestStream()) { - writeStream.Write(bytes, 0, bytes.Length); + writeStream.Write(request.ContentData, 0, request.ContentData.Length); } } @@ -75,43 +78,43 @@ namespace NzbDrone.Common.Http.Dispatchers switch (header.Key) { case "Accept": - webRequest.Accept = header.Value.ToString(); + webRequest.Accept = header.Value; break; case "Connection": - webRequest.Connection = header.Value.ToString(); + webRequest.Connection = header.Value; break; case "Content-Length": webRequest.ContentLength = Convert.ToInt64(header.Value); break; case "Content-Type": - webRequest.ContentType = header.Value.ToString(); + webRequest.ContentType = header.Value; break; case "Date": - webRequest.Date = (DateTime)header.Value; + webRequest.Date = HttpHeader.ParseDateTime(header.Value); break; case "Expect": - webRequest.Expect = header.Value.ToString(); + webRequest.Expect = header.Value; break; case "Host": - webRequest.Host = header.Value.ToString(); + webRequest.Host = header.Value; break; case "If-Modified-Since": - webRequest.IfModifiedSince = (DateTime)header.Value; + webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value); break; case "Range": throw new NotImplementedException(); case "Referer": - webRequest.Referer = header.Value.ToString(); + webRequest.Referer = header.Value; break; case "Transfer-Encoding": - webRequest.TransferEncoding = header.Value.ToString(); + webRequest.TransferEncoding = header.Value; break; case "User-Agent": throw new NotSupportedException("User-Agent other than Sonarr not allowed."); case "Proxy-Connection": throw new NotImplementedException(); default: - webRequest.Headers.Add(header.Key, header.Value.ToString()); + webRequest.Headers.Add(header.Key, header.Value); break; } } diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index 2c126372b..4735abe5a 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -4,9 +4,11 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Net; +using System.Text; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.TPL; diff --git a/src/NzbDrone.Common/Http/HttpException.cs b/src/NzbDrone.Common/Http/HttpException.cs index 626d17774..fbe78132f 100644 --- a/src/NzbDrone.Common/Http/HttpException.cs +++ b/src/NzbDrone.Common/Http/HttpException.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http public HttpResponse Response { get; private set; } public HttpException(HttpRequest request, HttpResponse response) - : base(string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url.ToString())) + : base(string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url.AbsoluteUri)) { Request = request; Response = response; diff --git a/src/NzbDrone.Common/Http/HttpFormData.cs b/src/NzbDrone.Common/Http/HttpFormData.cs new file mode 100644 index 000000000..1b5e6471c --- /dev/null +++ b/src/NzbDrone.Common/Http/HttpFormData.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Common.Http +{ + public class HttpFormData + { + public string Name { get; set; } + public string FileName { get; set; } + public byte[] ContentData { get; set; } + public string ContentType { get; set; } + } +} diff --git a/src/NzbDrone.Common/Http/HttpHeader.cs b/src/NzbDrone.Common/Http/HttpHeader.cs index 36d10a996..0db356feb 100644 --- a/src/NzbDrone.Common/Http/HttpHeader.cs +++ b/src/NzbDrone.Common/Http/HttpHeader.cs @@ -4,37 +4,92 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Text; using NzbDrone.Common.Extensions; +using System.Collections; +using System.Globalization; namespace NzbDrone.Common.Http { - public class HttpHeader : Dictionary + public class HttpHeader : NameValueCollection, IEnumerable>, IEnumerable { - public HttpHeader(NameValueCollection headers) : base(StringComparer.OrdinalIgnoreCase) + public HttpHeader(NameValueCollection headers) + : base(headers) { - foreach (var key in headers.AllKeys) + + } + + public HttpHeader() + { + + } + + public bool ContainsKey(string key) + { + key = key.ToLowerInvariant(); + return AllKeys.Any(v => v.ToLowerInvariant() == key); + } + + public string GetSingleValue(string key) + { + var values = GetValues(key); + if (values == null || values.Length == 0) + { + return null; + } + if (values.Length > 1) { - this[key] = headers[key]; + throw new ApplicationException(string.Format("Expected {0} to occur only once.", key)); } + + return values[0]; } - public HttpHeader() : base(StringComparer.OrdinalIgnoreCase) + protected T? GetSingleValue(string key, Func converter) where T : struct { + var value = GetSingleValue(key); + if (value == null) + { + return null; + } + return converter(value); + } + protected void SetSingleValue(string key, string value) + { + if (value == null) + { + Remove(key); + } + else + { + Set(key, value); + } + } + + protected void SetSingleValue(string key, T? value, Func converter = null) where T : struct + { + if (!value.HasValue) + { + Remove(key); + } + else if (converter != null) + { + Set(key, converter(value.Value)); + } + else + { + Set(key, value.Value.ToString()); + } } public long? ContentLength { get { - if (!ContainsKey("Content-Length")) - { - return null; - } - return Convert.ToInt64(this["Content-Length"]); + return GetSingleValue("Content-Length", Convert.ToInt64); } set { - this["Content-Length"] = value; + SetSingleValue("Content-Length", value); } } @@ -42,15 +97,11 @@ namespace NzbDrone.Common.Http { get { - if (!ContainsKey("Content-Type")) - { - return null; - } - return this["Content-Type"].ToString(); + return GetSingleValue("Content-Type"); } set { - this["Content-Type"] = value; + SetSingleValue("Content-Type", value); } } @@ -58,25 +109,36 @@ namespace NzbDrone.Common.Http { get { - if (!ContainsKey("Accept")) - { - return null; - } - return this["Accept"].ToString(); + return GetSingleValue("Accept"); } set { - this["Accept"] = value; + SetSingleValue("Accept", value); } } + public new IEnumerator> GetEnumerator() + { + return AllKeys.SelectMany(GetValues, (k, c) => new KeyValuePair(k, c)).ToList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return base.GetEnumerator(); + } + public Encoding GetEncodingFromContentType() + { + return GetEncodingFromContentType(ContentType ?? string.Empty); + } + + public static Encoding GetEncodingFromContentType(string contentType) { Encoding encoding = null; - if (ContentType.IsNotNullOrWhiteSpace()) + if (contentType.IsNotNullOrWhiteSpace()) { - var charset = ContentType.ToLowerInvariant() + var charset = contentType.ToLowerInvariant() .Split(';', '=', ' ') .SkipWhile(v => v != "charset") .Skip(1).FirstOrDefault(); @@ -99,5 +161,18 @@ namespace NzbDrone.Common.Http return encoding; } + + public static DateTime ParseDateTime(string value) + { + return DateTime.ParseExact(value, "R", CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AssumeUniversal); + } + + public static List> ParseCookies(string cookies) + { + return cookies.Split(';') + .Select(v => v.Trim().Split('=')) + .Select(v => new KeyValuePair(v[0], v[1])) + .ToList(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Common/Http/HttpRequest.cs b/src/NzbDrone.Common/Http/HttpRequest.cs index 6e8d1d5b3..5ff3564fb 100644 --- a/src/NzbDrone.Common/Http/HttpRequest.cs +++ b/src/NzbDrone.Common/Http/HttpRequest.cs @@ -1,19 +1,19 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Text; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Http { public class HttpRequest { - private readonly Dictionary _segments; - - public HttpRequest(string url, HttpAccept httpAccept = null) + public HttpRequest(string uri, HttpAccept httpAccept = null) { - UriBuilder = new UriBuilder(url); + UrlBuilder = new UriBuilder(uri); Headers = new HttpHeader(); - _segments = new Dictionary(); AllowAutoRedirect = true; Cookies = new Dictionary(); @@ -28,73 +28,41 @@ namespace NzbDrone.Common.Http } } - public UriBuilder UriBuilder { get; private set; } - - public Uri Url - { - get - { - var uri = UriBuilder.Uri.ToString(); - - foreach (var segment in _segments) - { - uri = uri.Replace(segment.Key, segment.Value); - } - - return new Uri(uri); - } - } - + public UriBuilder UrlBuilder { get; private set; } + public Uri Url { get { return UrlBuilder.Uri; } } public HttpMethod Method { get; set; } public HttpHeader Headers { get; set; } - public string Body { get; set; } + public byte[] ContentData { get; set; } + public string ContentSummary { get; set; } public NetworkCredential NetworkCredential { get; set; } public bool SuppressHttpError { get; set; } public bool AllowAutoRedirect { get; set; } public Dictionary Cookies { get; private set; } public bool StoreResponseCookie { get; set; } + public TimeSpan RequestTimeout { get; set; } public TimeSpan RateLimit { get; set; } public override string ToString() { - if (Body == null) + if (ContentSummary == null) { return string.Format("Req: [{0}] {1}", Method, Url); } - - return string.Format("Req: [{0}] {1} {2} {3}", Method, Url, Environment.NewLine, Body); - } - - public void AddSegment(string segment, string value) - { - var key = "{" + segment + "}"; - - if (!UriBuilder.Uri.ToString().Contains(key)) + else { - throw new InvalidOperationException("Segment " + key +" is not defined in Uri"); + return string.Format("Req: [{0}] {1}: {2}", Method, Url, ContentSummary); } - - _segments.Add(key, value); - } - - public void AddQueryParam(string segment, string value) - { - UriBuilder.SetQueryParam(segment, value); } - public void AddCookie(string key, string value) + public void SetContent(byte[] data) { - Cookies[key] = value; + ContentData = data; } - public void AddCookie(string cookies) + public void SetContent(string data) { - foreach (var pair in cookies.Split(';')) - { - var split = pair.Split('='); - - Cookies[split[0].Trim()] = split[1].Trim(); - } + var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType); + ContentData = encoding.GetBytes(data); } } } \ No newline at end of file diff --git a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs index ca3b83faf..280201359 100644 --- a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs @@ -1,33 +1,123 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net; +using System.Text; +using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Http { public class HttpRequestBuilder { - public Uri BaseUri { get; private set; } - public bool SupressHttpError { get; set; } + public HttpMethod Method { get; set; } + public HttpAccept HttpAccept { get; set; } + public Uri BaseUrl { get; private set; } + public string ResourceUrl { get; set; } + public List> QueryParams { get; private set; } + public List> SuffixQueryParams { get; private set; } + public Dictionary Segments { get; private set; } + public HttpHeader Headers { get; private set; } + public bool SuppressHttpError { get; set; } + public bool AllowAutoRedirect { get; set; } public NetworkCredential NetworkCredential { get; set; } + public Dictionary Cookies { get; private set; } public Action PostProcess { get; set; } - public HttpRequestBuilder(string baseUri) + public HttpRequestBuilder(string baseUrl) { - BaseUri = new Uri(baseUri); + BaseUrl = new Uri(baseUrl); + ResourceUrl = string.Empty; + Method = HttpMethod.GET; + QueryParams = new List>(); + SuffixQueryParams = new List>(); + Segments = new Dictionary(); + Headers = new HttpHeader(); + Cookies = new Dictionary(); } - public virtual HttpRequest Build(string path) + public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null) + : this(BuildBaseUrl(useHttps, host, port, urlBase)) { - if (BaseUri.ToString().EndsWith("/")) + + } + + public static string BuildBaseUrl(bool useHttps, string host, int port, string urlBase = null) + { + var protocol = useHttps ? "https" : "http"; + + if (urlBase.IsNotNullOrWhiteSpace() && !urlBase.StartsWith("/")) { - path = path.TrimStart('/'); + urlBase = "/" + urlBase; } - var request = new HttpRequest(BaseUri + path) + return string.Format("{0}://{1}:{2}{3}", protocol, host, port, urlBase); + } + + public virtual HttpRequestBuilder Clone() + { + var clone = MemberwiseClone() as HttpRequestBuilder; + clone.QueryParams = new List>(clone.QueryParams); + clone.SuffixQueryParams = new List>(clone.SuffixQueryParams); + clone.Segments = new Dictionary(clone.Segments); + clone.Headers = new HttpHeader(clone.Headers); + clone.Cookies = new Dictionary(clone.Cookies); + return clone; + } + + protected virtual Uri CreateUri() + { + var builder = new UriBuilder(new Uri(BaseUrl, ResourceUrl)); + + foreach (var queryParam in QueryParams.Concat(SuffixQueryParams)) { - SuppressHttpError = SupressHttpError, - NetworkCredential = NetworkCredential - }; + builder.SetQueryParam(queryParam.Key, queryParam.Value); + } + + if (Segments.Any()) + { + var url = builder.Uri.ToString(); + + foreach (var segment in Segments) + { + url = url.Replace(segment.Key, segment.Value); + } + + builder = new UriBuilder(url); + } + + return builder.Uri; + } + + protected virtual HttpRequest CreateRequest() + { + return new HttpRequest(CreateUri().ToString(), HttpAccept); + } + + protected virtual void Apply(HttpRequest request) + { + request.Method = Method; + request.SuppressHttpError = SuppressHttpError; + request.AllowAutoRedirect = AllowAutoRedirect; + request.NetworkCredential = NetworkCredential; + + foreach (var header in Headers) + { + request.Headers.Set(header.Key, header.Value); + } + + foreach (var cookie in Cookies) + { + request.Cookies[cookie.Key] = cookie.Value; + } + } + + public virtual HttpRequest Build() + { + var request = CreateRequest(); + + Apply(request); if (PostProcess != null) { @@ -36,5 +126,102 @@ namespace NzbDrone.Common.Http return request; } + + public IHttpRequestBuilderFactory CreateFactory() + { + return new HttpRequestBuilderFactory(this); + } + + public virtual HttpRequestBuilder Resource(string resourceUrl) + { + if (!ResourceUrl.IsNotNullOrWhiteSpace() || resourceUrl.StartsWith("/")) + { + ResourceUrl = resourceUrl.TrimStart('/'); + } + else + { + ResourceUrl = string.Format("{0}/{1}", ResourceUrl.TrimEnd('/'), resourceUrl); + } + + return this; + } + + public virtual HttpRequestBuilder Post() + { + Method = HttpMethod.POST; + + return this; + } + + public virtual HttpRequestBuilder Accept(HttpAccept accept) + { + HttpAccept = accept; + + return this; + } + + public virtual HttpRequestBuilder SetHeader(string name, string value) + { + Headers.Set(name, value); + + return this; + } + + public virtual HttpRequestBuilder AddQueryParam(string key, object value, bool replace = false) + { + if (replace) + { + QueryParams.RemoveAll(v => v.Key == key); + SuffixQueryParams.RemoveAll(v => v.Key == key); + } + + QueryParams.Add(key, value.ToString()); + + return this; + } + + public virtual HttpRequestBuilder AddSuffixQueryParam(string key, object value, bool replace = false) + { + if (replace) + { + QueryParams.RemoveAll(v => v.Key == key); + SuffixQueryParams.RemoveAll(v => v.Key == key); + } + + SuffixQueryParams.Add(new KeyValuePair(key, value.ToString())); + + return this; + } + + public virtual HttpRequestBuilder SetSegment(string segment, string value, bool dontCheck = false) + { + var key = string.Concat("{", segment, "}"); + + if (!dontCheck && !CreateUri().ToString().Contains(key)) + { + throw new InvalidOperationException(string.Format("Segment {0} is not defined in Uri", segment)); + } + + Segments[key] = value; + + return this; + } + + public virtual HttpRequestBuilder SetCookies(IEnumerable> cookies) + { + foreach (var cookie in cookies) + { + Cookies[cookie.Key] = cookie.Value; + } + + return this; + } + + public virtual HttpRequestBuilder SetCookie(string key, string value) + { + Cookies[key] = value; + + return this; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Common/Http/HttpRequestBuilderFactory.cs b/src/NzbDrone.Common/Http/HttpRequestBuilderFactory.cs new file mode 100644 index 000000000..142050790 --- /dev/null +++ b/src/NzbDrone.Common/Http/HttpRequestBuilderFactory.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Common.Http +{ + public interface IHttpRequestBuilderFactory + { + HttpRequestBuilder Create(); + } + + public class HttpRequestBuilderFactory : IHttpRequestBuilderFactory + { + private HttpRequestBuilder _rootBuilder; + + public HttpRequestBuilderFactory(HttpRequestBuilder rootBuilder) + { + SetRootBuilder(rootBuilder); + } + + protected HttpRequestBuilderFactory() + { + + } + + protected void SetRootBuilder(HttpRequestBuilder rootBuilder) + { + _rootBuilder = rootBuilder.Clone(); + } + + public HttpRequestBuilder Create() + { + return _rootBuilder.Clone(); + } + } +} diff --git a/src/NzbDrone.Common/Http/HttpResponse.cs b/src/NzbDrone.Common/Http/HttpResponse.cs index 32a9c10ac..65d9ff47a 100644 --- a/src/NzbDrone.Common/Http/HttpResponse.cs +++ b/src/NzbDrone.Common/Http/HttpResponse.cs @@ -1,11 +1,16 @@ using System; +using System.Collections.Generic; using System.Net; +using System.Text.RegularExpressions; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; namespace NzbDrone.Common.Http { public class HttpResponse { + private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled); + public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK) { Request = request; @@ -52,11 +57,27 @@ namespace NzbDrone.Common.Http } } + public Dictionary GetCookies() + { + var result = new Dictionary(); + + foreach (var cookie in Headers.GetValues("Set-Cookie")) + { + var match = RegexSetCookie.Match(cookie); + if (match.Success) + { + result[match.Groups[1].Value] = match.Groups[2].Value; + } + } + + return result; + } + public override string ToString() { var result = string.Format("Res: [{0}] {1} : {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode); - if (HasHttpError && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase)) + if (HasHttpError && Headers.ContentType.IsNotNullOrWhiteSpace() && !Headers.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase)) { result += Environment.NewLine + Content; } diff --git a/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs b/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs index 8ad7f6ade..f50e7d9a3 100644 --- a/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/JsonRpcRequestBuilder.cs @@ -7,32 +7,87 @@ namespace NzbDrone.Common.Http { public class JsonRpcRequestBuilder : HttpRequestBuilder { - public string Method { get; private set; } - public List Parameters { get; private set; } + public static HttpAccept JsonRpcHttpAccept = new HttpAccept("application/json-rpc, application/json"); + public static string JsonRpcContentType = "application/json-rpc"; - public JsonRpcRequestBuilder(string baseUri, string method, IEnumerable parameters) - : base (baseUri) + public string JsonMethod { get; private set; } + public List JsonParameters { get; private set; } + + public JsonRpcRequestBuilder(string baseUrl) + : base(baseUrl) + { + Method = HttpMethod.POST; + JsonParameters = new List(); + } + + public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable parameters) + : base (baseUrl) + { + Method = HttpMethod.POST; + JsonMethod = method; + JsonParameters = parameters.ToList(); + } + + public override HttpRequestBuilder Clone() + { + var clone = base.Clone() as JsonRpcRequestBuilder; + clone.JsonParameters = new List(JsonParameters); + return clone; + } + + public JsonRpcRequestBuilder Call(string method, params object[] parameters) { - Method = method; - Parameters = parameters.ToList(); + var clone = Clone() as JsonRpcRequestBuilder; + clone.JsonMethod = method; + clone.JsonParameters = parameters.ToList(); + return clone; } - public override HttpRequest Build(string path) + protected override void Apply(HttpRequest request) { - var request = base.Build(path); - request.Method = HttpMethod.POST; - request.Headers.Accept = "application/json-rpc, application/json"; - request.Headers.ContentType = "application/json-rpc"; + base.Apply(request); + + request.Headers.ContentType = JsonRpcContentType; + + var parameterData = new object[JsonParameters.Count]; + var parameterSummary = new string[JsonParameters.Count]; + + for (var i = 0; i < JsonParameters.Count; i++) + { + ConvertParameter(JsonParameters[i], out parameterData[i], out parameterSummary[i]); + } var message = new Dictionary(); message["jsonrpc"] = "2.0"; - message["method"] = Method; - message["params"] = Parameters; + message["method"] = JsonMethod; + message["params"] = parameterData; message["id"] = CreateNextId(); - request.Body = message.ToJson(); + request.SetContent(message.ToJson()); + + if (request.ContentSummary == null) + { + request.ContentSummary = string.Format("{0}({1})", JsonMethod, string.Join(", ", parameterSummary)); + } + } - return request; + private void ConvertParameter(object value, out object data, out string summary) + { + if (value is byte[]) + { + data = Convert.ToBase64String(value as byte[]); + summary = string.Format("[blob {0} bytes]", (value as byte[]).Length); + } + else if (value is Array && ((Array)value).Length > 0) + { + data = value; + summary = "[...]"; + } + else + { + data = value; + summary = data.ToJson(); + } } public string CreateNextId() diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index d424ae350..855975123 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -67,7 +67,7 @@ - + @@ -156,6 +156,7 @@ + @@ -168,6 +169,7 @@ Component + diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs index 9356a8e39..a7a4232e0 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/TorrentBlackholeFixture.cs @@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Subject.Download(remoteEpisode); - Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.ToString() == _downloadUrl)), Times.Once()); + Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Once()); Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); } @@ -128,7 +128,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Subject.Download(remoteEpisode); - Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.ToString() == _downloadUrl)), Times.Once()); + Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once()); Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs index 3dc4a93f9..933113f6b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs @@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Subject.Download(remoteEpisode); - Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.ToString() == _downloadUrl)), Times.Once()); + Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(_filePath), Times.Once()); Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); } @@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole Subject.Download(remoteEpisode); - Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.ToString() == _downloadUrl)), Times.Once()); + Mocker.GetMock().Verify(c => c.Get(It.Is(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once()); Mocker.GetMock().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once()); Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never()); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs index 0faf1441a..21130cfd1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/UTorrentTests/UTorrentFixture.cs @@ -110,7 +110,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests httpHeader["Location"] = "http://test.sonarr.tv/not-a-real-torrent.torrent"; Mocker.GetMock() - .Setup(s => s.Get(It.Is(h => h.Url.AbsoluteUri == _downloadUrl))) + .Setup(s => s.Get(It.Is(h => h.Url.ToString() == _downloadUrl))) .Returns(r => new HttpResponse(r, httpHeader, new byte[0], System.Net.HttpStatusCode.Found)); } diff --git a/src/NzbDrone.Core.Test/FluentTest.cs b/src/NzbDrone.Core.Test/FluentTest.cs index b418d2b3a..f9886700a 100644 --- a/src/NzbDrone.Core.Test/FluentTest.cs +++ b/src/NzbDrone.Core.Test/FluentTest.cs @@ -84,90 +84,6 @@ namespace NzbDrone.Core.Test dateTime.ToBestDateString().Should().Be(dateTime.ToShortDateString()); } - [Test] - public void ParentUriString_should_return_self_if_already_parent() - { - - var url = "http://www.sonarr.tv"; - var uri = new Uri(url); - - - var result = uri.ParentUriString(); - - //Resolve - result.Should().Be(url); - } - - [Test] - public void ParentUriString_should_return_parent_url_when_path_is_passed() - { - - var url = "http://www.sonarr.tv/test/"; - var uri = new Uri(url); - - - var result = uri.ParentUriString(); - - //Resolve - result.Should().Be("http://www.sonarr.tv"); - } - - [Test] - public void ParentUriString_should_return_parent_url_when_multiple_paths_are_passed() - { - - var url = "http://www.sonarr.tv/test/test2"; - var uri = new Uri(url); - - - var result = uri.ParentUriString(); - - //Resolve - result.Should().Be("http://www.sonarr.tv"); - } - - [Test] - public void ParentUriString_should_return_parent_url_when_url_with_query_string_is_passed() - { - - var url = "http://www.sonarr.tv/test.aspx?test=10"; - var uri = new Uri(url); - - - var result = uri.ParentUriString(); - - //Resolve - result.Should().Be("http://www.sonarr.tv"); - } - - [Test] - public void ParentUriString_should_return_parent_url_when_url_with_path_and_query_strings_is_passed() - { - - var url = "http://www.sonarr.tv/tester/test.aspx?test=10"; - var uri = new Uri(url); - - - var result = uri.ParentUriString(); - - //Resolve - result.Should().Be("http://www.sonarr.tv"); - } - - [Test] - public void ParentUriString_should_return_parent_url_when_url_with_query_strings_is_passed() - { - - var url = "http://www.sonarr.tv/test.aspx?test=10&test2=5"; - var uri = new Uri(url); - - - var result = uri.ParentUriString(); - - //Resolve - result.Should().Be("http://www.sonarr.tv"); - } - [Test] public void MaxOrDefault_should_return_zero_when_collection_is_empty() { diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index aa6d1aaa4..67eddc9b8 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.Framework { Mocker.SetConstant(new HttpProvider(TestLogger)); Mocker.SetConstant(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), TestLogger)); - Mocker.SetConstant(new DroneServicesHttpRequestBuilder()); + Mocker.SetConstant(new SonarrCloudRequestBuilder()); } } diff --git a/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs b/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs index 49d32b101..6d1778bdc 100644 --- a/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs +++ b/src/NzbDrone.Core/DataAugmentation/DailySeries/DailySeriesDataProxy.cs @@ -15,13 +15,13 @@ namespace NzbDrone.Core.DataAugmentation.DailySeries public class DailySeriesDataProxy : IDailySeriesDataProxy { private readonly IHttpClient _httpClient; - private readonly IDroneServicesRequestBuilder _requestBuilder; + private readonly IHttpRequestBuilderFactory _requestBuilder; private readonly Logger _logger; - public DailySeriesDataProxy(IHttpClient httpClient, IDroneServicesRequestBuilder requestBuilder, Logger logger) + public DailySeriesDataProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger) { _httpClient = httpClient; - _requestBuilder = requestBuilder; + _requestBuilder = requestBuilder.Services; _logger = logger; } @@ -29,7 +29,10 @@ namespace NzbDrone.Core.DataAugmentation.DailySeries { try { - var dailySeriesRequest = _requestBuilder.Build("dailyseries"); + var dailySeriesRequest = _requestBuilder.Create() + .Resource("/dailyseries") + .Build(); + var response = _httpClient.Get>(dailySeriesRequest); return response.Resource.Select(c => c.TvdbId); } diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs index 5fd62a5e1..735af870b 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingProxy.cs @@ -12,17 +12,20 @@ namespace NzbDrone.Core.DataAugmentation.Scene public class SceneMappingProxy : ISceneMappingProxy { private readonly IHttpClient _httpClient; - private readonly IDroneServicesRequestBuilder _requestBuilder; + private readonly IHttpRequestBuilderFactory _requestBuilder; - public SceneMappingProxy(IHttpClient httpClient, IDroneServicesRequestBuilder requestBuilder) + public SceneMappingProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder) { _httpClient = httpClient; - _requestBuilder = requestBuilder; + _requestBuilder = requestBuilder.Services; } public List Fetch() { - var request = _requestBuilder.Build("/scenemapping"); + var request = _requestBuilder.Create() + .Resource("/scenemapping") + .Build(); + return _httpClient.Get>(request).Resource; } } diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs index dde409322..5cf39c308 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemProxy.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using NLog; +using NzbDrone.Common.Cloud; using NzbDrone.Common.Http; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DataAugmentation.Xem.Model; @@ -18,32 +19,32 @@ namespace NzbDrone.Core.DataAugmentation.Xem public class XemProxy : IXemProxy { + private const string ROOT_URL = "http://thexem.de/map/"; + private readonly Logger _logger; private readonly IHttpClient _httpClient; - - private const string XEM_BASE_URL = "http://thexem.de/map/"; + private readonly IHttpRequestBuilderFactory _xemRequestBuilder; private static readonly string[] IgnoredErrors = { "no single connection", "no show with the tvdb_id" }; - private HttpRequestBuilder _xemRequestBuilder; - - public XemProxy(Logger logger, IHttpClient httpClient) + public XemProxy(IHttpClient httpClient, Logger logger) { - _logger = logger; _httpClient = httpClient; + _logger = logger; - _xemRequestBuilder = new HttpRequestBuilder(XEM_BASE_URL) - { - PostProcess = r => r.UriBuilder.SetQueryParam("origin", "tvdb") - }; + _xemRequestBuilder = new HttpRequestBuilder(ROOT_URL) + .AddSuffixQueryParam("origin", "tvdb") + .CreateFactory(); } - public List GetXemSeriesIds() { _logger.Debug("Fetching Series IDs from"); - var request = _xemRequestBuilder.Build("/havemap"); + var request = _xemRequestBuilder.Create() + .Resource("/havemap") + .Build(); + var response = _httpClient.Get>>(request).Resource; CheckForFailureResult(response); @@ -60,9 +61,10 @@ namespace NzbDrone.Core.DataAugmentation.Xem { _logger.Debug("Fetching Mappings for: {0}", id); - - var request = _xemRequestBuilder.Build("/all"); - request.UriBuilder.SetQueryParam("id", id); + var request = _xemRequestBuilder.Create() + .Resource("/all") + .AddQueryParam("id", id) + .Build(); var response = _httpClient.Get>>(request).Resource; @@ -73,8 +75,10 @@ namespace NzbDrone.Core.DataAugmentation.Xem { _logger.Debug("Fetching alternate names"); - var request = _xemRequestBuilder.Build("/allNames"); - request.UriBuilder.SetQueryParam("seasonNumbers", true); + var request = _xemRequestBuilder.Create() + .Resource("/allNames") + .AddQueryParam("seasonNumbers", true) + .Build(); var response = _httpClient.Get>>>(request).Resource; diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index d457ed816..f2845ad16 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -107,7 +107,7 @@ namespace NzbDrone.Core.Download if (response.StatusCode == HttpStatusCode.SeeOther || response.StatusCode == HttpStatusCode.Found) { - var locationHeader = (string)response.Headers.GetValueOrDefault("Location", null); + var locationHeader = response.Headers.GetSingleValue("Location"); _logger.Trace("Torrent request is being redirected to: {0}", locationHeader); diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index bb6dcecae..6e2e3d2b2 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -63,11 +63,6 @@ namespace NzbDrone.Core return dateTime.ToShortDateString(); } - public static string ParentUriString(this Uri uri) - { - return uri.AbsoluteUri.Remove(uri.AbsoluteUri.Length - string.Join("", uri.Segments).Length - uri.Query.Length); - } - public static int MaxOrDefault(this IEnumerable ints) { if (ints == null) diff --git a/src/NzbDrone.Core/Http/TorcacheHttpInterceptor.cs b/src/NzbDrone.Core/Http/TorcacheHttpInterceptor.cs index e5b19e4a2..857cf4048 100644 --- a/src/NzbDrone.Core/Http/TorcacheHttpInterceptor.cs +++ b/src/NzbDrone.Core/Http/TorcacheHttpInterceptor.cs @@ -13,9 +13,9 @@ namespace NzbDrone.Core.Http { // torcache behaves strangely when it has query params and/or no Referer or browser User-Agent. // It's a bit vague, and we don't need the query params. So we remove the query params and set a Referer to be safe. - if (request.Url.Host == "torcache.net") + if (request.UrlBuilder.Host == "torcache.net") { - request.UriBuilder.Query = string.Empty; + request.UrlBuilder.Query = string.Empty; request.Headers.Add("Referer", request.Url.Scheme + @"://torcache.net/"); } diff --git a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs index ff9165b6a..1152addb5 100644 --- a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs @@ -48,7 +48,10 @@ namespace NzbDrone.Core.Indexers.BitMeTv { var request = new IndexerRequest(string.Format("{0}/rss.php?uid={1}&passkey={2}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.UserId, Settings.RssPasskey), HttpAccept.Html); - request.HttpRequest.AddCookie(Settings.Cookie); + foreach (var cookie in HttpHeader.ParseCookies(Settings.Cookie)) + { + request.HttpRequest.Cookies[cookie.Key] = cookie.Value; + } yield return request; } diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs index 3747c3e10..fc87080b4 100644 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs @@ -172,14 +172,15 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet parameters = new BroadcastheNetTorrentQuery(); } - var builder = new JsonRpcRequestBuilder(Settings.BaseUrl, "getTorrents", new object[] { Settings.ApiKey, parameters, PageSize, 0 }); - builder.SupressHttpError = true; + var builder = new JsonRpcRequestBuilder(Settings.BaseUrl) + .Call("getTorrents", Settings.ApiKey, parameters, PageSize, 0); + builder.SuppressHttpError = true; for (var page = 0; page < maxPages;page++) { - builder.Parameters[3] = page * PageSize; + builder.JsonParameters[3] = page * PageSize; - yield return new IndexerRequest(builder.Build("")); + yield return new IndexerRequest(builder.Build()); } } } diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index 9adcf1c46..d03719eee 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -114,8 +114,9 @@ namespace NzbDrone.Core.Indexers.HDBits private IEnumerable GetRequest(TorrentQuery query) { - var builder = new HttpRequestBuilder(Settings.BaseUrl); - var request = builder.Build("/api/torrents"); + var request = new HttpRequestBuilder(Settings.BaseUrl) + .Resource("/api/torrents") + .Build(); request.Method = HttpMethod.POST; const string appJson = "application/json"; @@ -125,7 +126,7 @@ namespace NzbDrone.Core.Indexers.HDBits query.Username = Settings.Username; query.Passkey = Settings.ApiKey; - request.Body = query.ToJson(); + request.SetContent(query.ToJson()); yield return new IndexerRequest(request); } diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 9b645de5e..b206de969 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -137,7 +137,7 @@ namespace NzbDrone.Core.Indexers foreach (var request in pageableRequest) { - url = request.Url.ToString(); + url = request.Url.AbsoluteUri; var page = FetchPage(request, parser); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs index d71e84434..5e8b603ca 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Indexers.Newznab throw new ApiKeyException("Invalid API key"); } - if (!indexerResponse.Request.Url.ToString().Contains("apikey=") && (errorMessage == "Missing parameter" || errorMessage.Contains("apikey"))) + if (!indexerResponse.Request.Url.AbsoluteUri.Contains("apikey=") && (errorMessage == "Missing parameter" || errorMessage.Contains("apikey"))) { throw new ApiKeyException("Indexer requires an API key"); } diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs index da4c9bba2..2319ac207 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs @@ -75,30 +75,34 @@ namespace NzbDrone.Core.Indexers.Rarbg private IEnumerable GetPagedRequests(string mode, int? tvdbId, string query, params object[] args) { + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl) + .Resource("/pubapi_v2.php") + .Accept(HttpAccept.Json); + var httpRequest = new HttpRequest(Settings.BaseUrl + "/pubapi_v2.php", HttpAccept.Json); - httpRequest.AddQueryParam("mode", mode); + requestBuilder.AddQueryParam("mode", mode); if (tvdbId.HasValue) { - httpRequest.AddQueryParam("search_tvdb", tvdbId.Value.ToString()); + requestBuilder.AddQueryParam("search_tvdb", tvdbId.Value); } if (query.IsNotNullOrWhiteSpace()) { - httpRequest.AddQueryParam("search_string", string.Format(query, args)); + requestBuilder.AddQueryParam("search_string", string.Format(query, args)); } if (!Settings.RankedOnly) { - httpRequest.AddQueryParam("ranked", "0"); + requestBuilder.AddQueryParam("ranked", "0"); } - httpRequest.AddQueryParam("category", "18;41"); - httpRequest.AddQueryParam("limit", "100"); - httpRequest.AddQueryParam("token", _tokenProvider.GetToken(Settings)); - httpRequest.AddQueryParam("format", "json_extended"); - httpRequest.AddQueryParam("app_id", "Sonarr"); + requestBuilder.AddQueryParam("category", "18;41"); + requestBuilder.AddQueryParam("limit", "100"); + requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings)); + requestBuilder.AddQueryParam("format", "json_extended"); + requestBuilder.AddQueryParam("app_id", "Sonarr"); yield return new IndexerRequest(httpRequest); } diff --git a/src/NzbDrone.Core/Indexers/RssParser.cs b/src/NzbDrone.Core/Indexers/RssParser.cs index e4b785622..1ccb5ed75 100644 --- a/src/NzbDrone.Core/Indexers/RssParser.cs +++ b/src/NzbDrone.Core/Indexers/RssParser.cs @@ -266,18 +266,18 @@ namespace NzbDrone.Core.Indexers try { - var uri = new Uri(value, UriKind.RelativeOrAbsolute); + var url = new Uri(value, UriKind.RelativeOrAbsolute); - if (!uri.IsAbsoluteUri) + if (!url.IsAbsoluteUri) { - uri = new Uri(_indexerResponse.HttpRequest.Url, uri); + url = new Uri(_indexerResponse.HttpRequest.Url, url); } - return uri.AbsoluteUri; + return url.AbsoluteUri; } catch (Exception ex) { - _logger.Debug(ex, string.Format("Failed to parse Uri {0}, ignoring.", value)); + _logger.Debug(ex, string.Format("Failed to parse Url {0}, ignoring.", value)); return null; } } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs index ea5526cf9..b72862a76 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs @@ -50,7 +50,10 @@ namespace NzbDrone.Core.Indexers.TorrentRss if (Settings.Cookie.IsNotNullOrWhiteSpace()) { - request.HttpRequest.AddCookie(Settings.Cookie); + foreach (var cookie in HttpHeader.ParseCookies(Settings.Cookie)) + { + request.HttpRequest.Cookies[cookie.Key] = cookie.Value; + } } yield return request; diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 50ace7926..58d1593f8 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Indexers.Torznab if (code >= 100 && code <= 199) throw new ApiKeyException("Invalid API key"); - if (!indexerResponse.Request.Url.ToString().Contains("apikey=") && errorMessage == "Missing parameter") + if (!indexerResponse.Request.Url.AbsoluteUri.Contains("apikey=") && errorMessage == "Missing parameter") { throw new ApiKeyException("Indexer requires an API key"); } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index e8c570600..a321aad68 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using NLog; +using NzbDrone.Common.Cloud; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Exceptions; @@ -16,20 +17,23 @@ namespace NzbDrone.Core.MetadataSource.SkyHook { private readonly IHttpClient _httpClient; private readonly Logger _logger; - private readonly HttpRequestBuilder _requestBuilder; - public SkyHookProxy(IHttpClient httpClient, Logger logger) + private readonly IHttpRequestBuilderFactory _requestBuilder; + + public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger) { _httpClient = httpClient; + _requestBuilder = requestBuilder.SkyHookTvdb; _logger = logger; - - _requestBuilder = new HttpRequestBuilder("http://skyhook.sonarr.tv/v1/tvdb/{route}/en/"); } public Tuple> GetSeriesInfo(int tvdbSeriesId) { - var httpRequest = _requestBuilder.Build(tvdbSeriesId.ToString()); - httpRequest.AddSegment("route", "shows"); + var httpRequest = _requestBuilder.Create() + .SetSegment("route", "shows") + .Resource(tvdbSeriesId.ToString()) + .Build(); + httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; @@ -81,9 +85,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } var term = System.Web.HttpUtility.UrlEncode((title.ToLower().Trim())); - var httpRequest = _requestBuilder.Build("?term={term}"); - httpRequest.AddSegment("route", "search"); - httpRequest.AddSegment("term", term); + var httpRequest = _requestBuilder.Create() + .SetSegment("route", "search") + .AddQueryParam("term", title.ToLower().Trim()) + .Build(); var httpResponse = _httpClient.Get>(httpRequest); diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs index 80bdf5e2a..f9bd25f07 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowserProxy.cs @@ -21,15 +21,14 @@ namespace NzbDrone.Core.Notifications.MediaBrowser { var path = "/Notifications/Admin"; var request = BuildRequest(path, settings); + request.Headers.ContentType = "application/json"; - request.Body = new + request.SetContent(new { Name = title, Description = message, ImageUrl = "https://raw.github.com/NzbDrone/NzbDrone/develop/Logo/64.png" - }.ToJson(); - - request.Headers.ContentType = "application/json"; + }.ToJson()); ProcessRequest(request, settings); } @@ -58,7 +57,7 @@ namespace NzbDrone.Core.Notifications.MediaBrowser { var url = string.Format(@"http://{0}/mediabrowser", settings.Address); - return new HttpRequestBuilder(url).Build(path); + return new HttpRequestBuilder(url).Resource(path).Build(); } private void CheckForError(HttpResponse response) diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index 86a4f6299..5abb55726 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -15,20 +15,22 @@ namespace NzbDrone.Core.Update public class UpdatePackageProvider : IUpdatePackageProvider { private readonly IHttpClient _httpClient; - private readonly IDroneServicesRequestBuilder _requestBuilder; + private readonly IHttpRequestBuilderFactory _requestBuilder; - public UpdatePackageProvider(IHttpClient httpClient, IDroneServicesRequestBuilder requestBuilder) + public UpdatePackageProvider(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder) { _httpClient = httpClient; - _requestBuilder = requestBuilder; + _requestBuilder = requestBuilder.Services; } public UpdatePackage GetLatestUpdate(string branch, Version currentVersion) { - var request = _requestBuilder.Build("/update/{branch}"); - request.UriBuilder.SetQueryParam("version", currentVersion); - request.UriBuilder.SetQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()); - request.AddSegment("branch", branch); + var request = _requestBuilder.Create() + .Resource("/update/{branch}") + .AddQueryParam("version", currentVersion) + .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) + .SetSegment("branch", branch) + .Build(); var update = _httpClient.Get(request).Resource; @@ -39,10 +41,12 @@ namespace NzbDrone.Core.Update public List GetRecentUpdates(string branch, Version currentVersion) { - var request = _requestBuilder.Build("/update/{branch}/changes"); - request.UriBuilder.SetQueryParam("version", currentVersion); - request.UriBuilder.SetQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()); - request.AddSegment("branch", branch); + var request = _requestBuilder.Create() + .Resource("/update/{branch}/changes") + .AddQueryParam("version", currentVersion) + .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) + .SetSegment("branch", branch) + .Build(); var updates = _httpClient.Get>(request);