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).
pull/4/head
Taloth Saldono 8 years ago
parent 7818f0c59b
commit 2ffbbb0e71

@ -58,18 +58,20 @@ namespace NzbDrone.Common.Test.Http
var response = Subject.Get<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
response.Resource.Url.Should().Be(request.Url.ToString()); response.Resource.Url.Should().Be(request.Url.AbsoluteUri);
} }
[Test] [Test]
public void should_execute_simple_post() public void should_execute_simple_post()
{ {
var message = "{ my: 1 }";
var request = new HttpRequest("http://eu.httpbin.org/post"); var request = new HttpRequest("http://eu.httpbin.org/post");
request.Body = "{ my: 1 }"; request.SetContent(message);
var response = Subject.Post<HttpBinResource>(request); var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(request.Body); response.Resource.Data.Should().Be(message);
} }
[TestCase("gzip")] [TestCase("gzip")]
@ -162,7 +164,7 @@ namespace NzbDrone.Common.Test.Http
public void should_send_cookie() public void should_send_cookie()
{ {
var request = new HttpRequest("http://eu.httpbin.org/get"); var request = new HttpRequest("http://eu.httpbin.org/get");
request.AddCookie("my", "cookie"); request.Cookies["my"] = "cookie";
var response = Subject.Get<HttpBinResource>(request); var response = Subject.Get<HttpBinResource>(request);
@ -176,7 +178,7 @@ namespace NzbDrone.Common.Test.Http
public void GivenOldCookie() public void GivenOldCookie()
{ {
var oldRequest = new HttpRequest("http://eu.httpbin.org/get"); 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<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.Resolve<Logger>()); var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<ICacheManager>(), Mocker.Resolve<IRateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.Resolve<Logger>());
@ -260,7 +262,7 @@ namespace NzbDrone.Common.Test.Http
var requestSet = new HttpRequest("http://eu.httpbin.org/cookies/set?my=cookie"); var requestSet = new HttpRequest("http://eu.httpbin.org/cookies/set?my=cookie");
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true; requestSet.StoreResponseCookie = true;
requestSet.AddCookie("my", "oldcookie"); requestSet.Cookies["my"] = "oldcookie";
var responseSet = Subject.Get(requestSet); 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 // 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 malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Wed, 13-Jul-16 16:19:50 GMT; path=/; HttpOnly";
string url = "http://eu.httpbin.org/response-headers?Set-Cookie=" + var requestSet = new HttpRequestBuilder("http://eu.httpbin.org/response-headers")
System.Uri.EscapeUriString(malformedCookie); .AddQueryParam("Set-Cookie", malformedCookie)
.Build();
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false; requestSet.AllowAutoRedirect = false;
requestSet.StoreResponseCookie = true; 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 public class HttpBinResource

@ -1,4 +1,5 @@
using FluentAssertions; using System;
using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@ -8,14 +9,32 @@ namespace NzbDrone.Common.Test.Http
[TestFixture] [TestFixture]
public class HttpRequestBuilderFixture : TestBase 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<InvalidOperationException>(() => requestBuilder.SetSegment("seg2", "dir"));
}
[Test] [Test]
public void should_remove_duplicated_slashes() public void should_remove_duplicated_slashes()
{ {
var builder = new HttpRequestBuilder("http://domain/"); 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/");
} }
} }

@ -8,22 +8,5 @@ namespace NzbDrone.Common.Test.Http
[TestFixture] [TestFixture]
public class HttpRequestFixture 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<InvalidOperationException>(() => request.AddSegment("seg2", "dir"));
}
} }
} }

@ -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)
{
}
}
}

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

@ -26,7 +26,7 @@ namespace NzbDrone.Common.Extensions
public static void Add<TKey, TValue>(this ICollection<KeyValuePair<TKey, TValue>> collection, TKey key, TValue value) public static void Add<TKey, TValue>(this ICollection<KeyValuePair<TKey, TValue>> collection, TKey key, TValue value)
{ {
collection.Add(key, value); collection.Add(new KeyValuePair<TKey, TValue>(key, value));
} }
} }
} }

@ -86,6 +86,11 @@ namespace NzbDrone.Common.Http.Dispatchers
curlEasy.UserAgent = UserAgentBuilder.UserAgent; curlEasy.UserAgent = UserAgentBuilder.UserAgent;
curlEasy.FollowLocation = request.AllowAutoRedirect; curlEasy.FollowLocation = request.AllowAutoRedirect;
if (request.RequestTimeout != TimeSpan.Zero)
{
curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds);
}
if (OsInfo.IsWindows) if (OsInfo.IsWindows)
{ {
curlEasy.CaInfo = "curl-ca-bundle.crt"; curlEasy.CaInfo = "curl-ca-bundle.crt";
@ -96,11 +101,10 @@ namespace NzbDrone.Common.Http.Dispatchers
curlEasy.Cookie = cookies.GetCookieHeader(request.Url); 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.ContentData.Length;
curlEasy.PostFieldSize = request.Body.Length; curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v)));
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

@ -23,19 +23,22 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.ContentLength = 0; webRequest.ContentLength = 0;
webRequest.CookieContainer = cookies; webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
{
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
}
if (request.Headers != null) if (request.Headers != null)
{ {
AddRequestHeaders(webRequest, request.Headers); AddRequestHeaders(webRequest, request.Headers);
} }
if (!request.Body.IsNullOrWhiteSpace()) if (request.ContentData != null)
{ {
var bytes = request.Headers.GetEncodingFromContentType().GetBytes(request.Body.ToCharArray()); webRequest.ContentLength = request.ContentData.Length;
webRequest.ContentLength = bytes.Length;
using (var writeStream = webRequest.GetRequestStream()) 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) switch (header.Key)
{ {
case "Accept": case "Accept":
webRequest.Accept = header.Value.ToString(); webRequest.Accept = header.Value;
break; break;
case "Connection": case "Connection":
webRequest.Connection = header.Value.ToString(); webRequest.Connection = header.Value;
break; break;
case "Content-Length": case "Content-Length":
webRequest.ContentLength = Convert.ToInt64(header.Value); webRequest.ContentLength = Convert.ToInt64(header.Value);
break; break;
case "Content-Type": case "Content-Type":
webRequest.ContentType = header.Value.ToString(); webRequest.ContentType = header.Value;
break; break;
case "Date": case "Date":
webRequest.Date = (DateTime)header.Value; webRequest.Date = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Expect": case "Expect":
webRequest.Expect = header.Value.ToString(); webRequest.Expect = header.Value;
break; break;
case "Host": case "Host":
webRequest.Host = header.Value.ToString(); webRequest.Host = header.Value;
break; break;
case "If-Modified-Since": case "If-Modified-Since":
webRequest.IfModifiedSince = (DateTime)header.Value; webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
break; break;
case "Range": case "Range":
throw new NotImplementedException(); throw new NotImplementedException();
case "Referer": case "Referer":
webRequest.Referer = header.Value.ToString(); webRequest.Referer = header.Value;
break; break;
case "Transfer-Encoding": case "Transfer-Encoding":
webRequest.TransferEncoding = header.Value.ToString(); webRequest.TransferEncoding = header.Value;
break; break;
case "User-Agent": case "User-Agent":
throw new NotSupportedException("User-Agent other than Sonarr not allowed."); throw new NotSupportedException("User-Agent other than Sonarr not allowed.");
case "Proxy-Connection": case "Proxy-Connection":
throw new NotImplementedException(); throw new NotImplementedException();
default: default:
webRequest.Headers.Add(header.Key, header.Value.ToString()); webRequest.Headers.Add(header.Key, header.Value);
break; break;
} }
} }

@ -4,9 +4,11 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text;
using NLog; 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.Http.Dispatchers; using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;

@ -8,7 +8,7 @@ namespace NzbDrone.Common.Http
public HttpResponse Response { get; private set; } public HttpResponse Response { get; private set; }
public HttpException(HttpRequest request, HttpResponse response) 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; Request = request;
Response = response; Response = response;

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

@ -4,37 +4,92 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Text; using System.Text;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using System.Collections;
using System.Globalization;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public class HttpHeader : Dictionary<string, object> public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, 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<T>(string key, Func<string, T> 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<T>(string key, T? value, Func<T, string> 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 public long? ContentLength
{ {
get get
{ {
if (!ContainsKey("Content-Length")) return GetSingleValue("Content-Length", Convert.ToInt64);
{
return null;
}
return Convert.ToInt64(this["Content-Length"]);
} }
set set
{ {
this["Content-Length"] = value; SetSingleValue("Content-Length", value);
} }
} }
@ -42,15 +97,11 @@ namespace NzbDrone.Common.Http
{ {
get get
{ {
if (!ContainsKey("Content-Type")) return GetSingleValue("Content-Type");
{
return null;
}
return this["Content-Type"].ToString();
} }
set set
{ {
this["Content-Type"] = value; SetSingleValue("Content-Type", value);
} }
} }
@ -58,25 +109,36 @@ namespace NzbDrone.Common.Http
{ {
get get
{ {
if (!ContainsKey("Accept")) return GetSingleValue("Accept");
{
return null;
}
return this["Accept"].ToString();
} }
set set
{ {
this["Accept"] = value; SetSingleValue("Accept", value);
} }
} }
public new IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
return AllKeys.SelectMany(GetValues, (k, c) => new KeyValuePair<string, string>(k, c)).ToList().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return base.GetEnumerator();
}
public Encoding GetEncodingFromContentType() public Encoding GetEncodingFromContentType()
{
return GetEncodingFromContentType(ContentType ?? string.Empty);
}
public static Encoding GetEncodingFromContentType(string contentType)
{ {
Encoding encoding = null; Encoding encoding = null;
if (ContentType.IsNotNullOrWhiteSpace()) if (contentType.IsNotNullOrWhiteSpace())
{ {
var charset = ContentType.ToLowerInvariant() var charset = contentType.ToLowerInvariant()
.Split(';', '=', ' ') .Split(';', '=', ' ')
.SkipWhile(v => v != "charset") .SkipWhile(v => v != "charset")
.Skip(1).FirstOrDefault(); .Skip(1).FirstOrDefault();
@ -99,5 +161,18 @@ namespace NzbDrone.Common.Http
return encoding; return encoding;
} }
public static DateTime ParseDateTime(string value)
{
return DateTime.ParseExact(value, "R", CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AssumeUniversal);
}
public static List<KeyValuePair<string, string>> ParseCookies(string cookies)
{
return cookies.Split(';')
.Select(v => v.Trim().Split('='))
.Select(v => new KeyValuePair<string, string>(v[0], v[1]))
.ToList();
}
} }
} }

@ -1,19 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Text;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public class HttpRequest public class HttpRequest
{ {
private readonly Dictionary<string, string> _segments; public HttpRequest(string uri, HttpAccept httpAccept = null)
public HttpRequest(string url, HttpAccept httpAccept = null)
{ {
UriBuilder = new UriBuilder(url); UrlBuilder = new UriBuilder(uri);
Headers = new HttpHeader(); Headers = new HttpHeader();
_segments = new Dictionary<string, string>();
AllowAutoRedirect = true; AllowAutoRedirect = true;
Cookies = new Dictionary<string, string>(); Cookies = new Dictionary<string, string>();
@ -28,73 +28,41 @@ namespace NzbDrone.Common.Http
} }
} }
public UriBuilder UriBuilder { get; private set; } public UriBuilder UrlBuilder { get; private set; }
public Uri Url { get { return UrlBuilder.Uri; } }
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 HttpMethod Method { get; set; } public HttpMethod Method { get; set; }
public HttpHeader Headers { 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 NetworkCredential NetworkCredential { get; set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public Dictionary<string, string> Cookies { get; private set; } public Dictionary<string, string> Cookies { get; private set; }
public bool StoreResponseCookie { get; set; } public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; } public TimeSpan RateLimit { get; set; }
public override string ToString() public override string ToString()
{ {
if (Body == null) if (ContentSummary == null)
{ {
return string.Format("Req: [{0}] {1}", Method, Url); return string.Format("Req: [{0}] {1}", Method, Url);
} }
else
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))
{ {
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 encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
{ ContentData = encoding.GetBytes(data);
var split = pair.Split('=');
Cookies[split[0].Trim()] = split[1].Trim();
}
} }
} }
} }

@ -1,33 +1,123 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Text;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public class HttpRequestBuilder public class HttpRequestBuilder
{ {
public Uri BaseUri { get; private set; } public HttpMethod Method { get; set; }
public bool SupressHttpError { get; set; } public HttpAccept HttpAccept { get; set; }
public Uri BaseUrl { get; private set; }
public string ResourceUrl { get; set; }
public List<KeyValuePair<string, string>> QueryParams { get; private set; }
public List<KeyValuePair<string, string>> SuffixQueryParams { get; private set; }
public Dictionary<string, string> 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 NetworkCredential NetworkCredential { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public Action<HttpRequest> PostProcess { get; set; } public Action<HttpRequest> 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<KeyValuePair<string, string>>();
SuffixQueryParams = new List<KeyValuePair<string, string>>();
Segments = new Dictionary<string, string>();
Headers = new HttpHeader();
Cookies = new Dictionary<string, string>();
} }
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<KeyValuePair<string, string>>(clone.QueryParams);
clone.SuffixQueryParams = new List<KeyValuePair<string, string>>(clone.SuffixQueryParams);
clone.Segments = new Dictionary<string, string>(clone.Segments);
clone.Headers = new HttpHeader(clone.Headers);
clone.Cookies = new Dictionary<string, string>(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, builder.SetQueryParam(queryParam.Key, queryParam.Value);
NetworkCredential = NetworkCredential }
};
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) if (PostProcess != null)
{ {
@ -36,5 +126,102 @@ namespace NzbDrone.Common.Http
return request; 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<string, string>(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<KeyValuePair<string, string>> 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;
}
} }
} }

@ -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();
}
}
}

@ -1,11 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Text.RegularExpressions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
namespace NzbDrone.Common.Http namespace NzbDrone.Common.Http
{ {
public class HttpResponse public class HttpResponse
{ {
private static readonly Regex RegexSetCookie = new Regex("^(.*?)=(.*?)(?:;|$)", RegexOptions.Compiled);
public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK) public HttpResponse(HttpRequest request, HttpHeader headers, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK)
{ {
Request = request; Request = request;
@ -52,11 +57,27 @@ namespace NzbDrone.Common.Http
} }
} }
public Dictionary<string, string> GetCookies()
{
var result = new Dictionary<string, string>();
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() public override string ToString()
{ {
var result = string.Format("Res: [{0}] {1} : {2}.{3}", Request.Method, Request.Url, (int)StatusCode, StatusCode); 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; result += Environment.NewLine + Content;
} }

@ -7,32 +7,87 @@ namespace NzbDrone.Common.Http
{ {
public class JsonRpcRequestBuilder : HttpRequestBuilder public class JsonRpcRequestBuilder : HttpRequestBuilder
{ {
public string Method { get; private set; } public static HttpAccept JsonRpcHttpAccept = new HttpAccept("application/json-rpc, application/json");
public List<object> Parameters { get; private set; } public static string JsonRpcContentType = "application/json-rpc";
public JsonRpcRequestBuilder(string baseUri, string method, IEnumerable<object> parameters) public string JsonMethod { get; private set; }
: base (baseUri) public List<object> JsonParameters { get; private set; }
public JsonRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.POST;
JsonParameters = new List<object>();
}
public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable<object> 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<object>(JsonParameters);
return clone;
}
public JsonRpcRequestBuilder Call(string method, params object[] parameters)
{ {
Method = method; var clone = Clone() as JsonRpcRequestBuilder;
Parameters = parameters.ToList(); 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); base.Apply(request);
request.Method = HttpMethod.POST;
request.Headers.Accept = "application/json-rpc, application/json"; request.Headers.ContentType = JsonRpcContentType;
request.Headers.ContentType = "application/json-rpc";
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<string, object>(); var message = new Dictionary<string, object>();
message["jsonrpc"] = "2.0"; message["jsonrpc"] = "2.0";
message["method"] = Method; message["method"] = JsonMethod;
message["params"] = Parameters; message["params"] = parameterData;
message["id"] = CreateNextId(); 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() public string CreateNextId()

@ -67,7 +67,7 @@
<Compile Include="Cache\CachedDictionary.cs" /> <Compile Include="Cache\CachedDictionary.cs" />
<Compile Include="Cache\ICached.cs" /> <Compile Include="Cache\ICached.cs" />
<Compile Include="Cache\ICachedDictionary.cs" /> <Compile Include="Cache\ICachedDictionary.cs" />
<Compile Include="Cloud\CloudClient.cs" /> <Compile Include="Cloud\SonarrCloudRequestBuilder.cs" />
<Compile Include="Composition\Container.cs" /> <Compile Include="Composition\Container.cs" />
<Compile Include="Composition\ContainerBuilderBase.cs" /> <Compile Include="Composition\ContainerBuilderBase.cs" />
<Compile Include="Composition\IContainer.cs" /> <Compile Include="Composition\IContainer.cs" />
@ -156,6 +156,7 @@
<Compile Include="Http\HttpAccept.cs" /> <Compile Include="Http\HttpAccept.cs" />
<Compile Include="Http\HttpClient.cs" /> <Compile Include="Http\HttpClient.cs" />
<Compile Include="Http\HttpException.cs" /> <Compile Include="Http\HttpException.cs" />
<Compile Include="Http\HttpFormData.cs" />
<Compile Include="Http\HttpHeader.cs" /> <Compile Include="Http\HttpHeader.cs" />
<Compile Include="Http\HttpMethod.cs" /> <Compile Include="Http\HttpMethod.cs" />
<Compile Include="Http\HttpProvider.cs" /> <Compile Include="Http\HttpProvider.cs" />
@ -168,6 +169,7 @@
<SubType>Component</SubType> <SubType>Component</SubType>
</Compile> </Compile>
<Compile Include="Http\HttpRequestBuilder.cs" /> <Compile Include="Http\HttpRequestBuilder.cs" />
<Compile Include="Http\HttpRequestBuilderFactory.cs" />
<Compile Include="Http\TooManyRequestsException.cs" /> <Compile Include="Http\TooManyRequestsException.cs" />
<Compile Include="Http\UriExtensions.cs" /> <Compile Include="Http\UriExtensions.cs" />
<Compile Include="Extensions\IEnumerableExtensions.cs" /> <Compile Include="Extensions\IEnumerableExtensions.cs" />

@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Subject.Download(remoteEpisode); Subject.Download(remoteEpisode);
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.ToString() == _downloadUrl)), Times.Once()); Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once()); Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never()); Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never());
} }
@ -128,7 +128,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Subject.Download(remoteEpisode); Subject.Download(remoteEpisode);
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.ToString() == _downloadUrl)), Times.Once()); Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once()); Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never()); Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never());
} }

@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Subject.Download(remoteEpisode); Subject.Download(remoteEpisode);
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.ToString() == _downloadUrl)), Times.Once()); Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once()); Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(_filePath), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never()); Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never());
} }
@ -109,7 +109,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
Subject.Download(remoteEpisode); Subject.Download(remoteEpisode);
Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.ToString() == _downloadUrl)), Times.Once()); Mocker.GetMock<IHttpClient>().Verify(c => c.Get(It.Is<HttpRequest>(v => v.Url.AbsoluteUri == _downloadUrl)), Times.Once());
Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once()); Mocker.GetMock<IDiskProvider>().Verify(c => c.OpenWriteStream(expectedFilename), Times.Once());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never()); Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never());
} }

@ -110,7 +110,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.UTorrentTests
httpHeader["Location"] = "http://test.sonarr.tv/not-a-real-torrent.torrent"; httpHeader["Location"] = "http://test.sonarr.tv/not-a-real-torrent.torrent";
Mocker.GetMock<IHttpClient>() Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.Is<HttpRequest>(h => h.Url.AbsoluteUri == _downloadUrl))) .Setup(s => s.Get(It.Is<HttpRequest>(h => h.Url.ToString() == _downloadUrl)))
.Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new byte[0], System.Net.HttpStatusCode.Found)); .Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new byte[0], System.Net.HttpStatusCode.Found));
} }

@ -84,90 +84,6 @@ namespace NzbDrone.Core.Test
dateTime.ToBestDateString().Should().Be(dateTime.ToShortDateString()); 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] [Test]
public void MaxOrDefault_should_return_zero_when_collection_is_empty() public void MaxOrDefault_should_return_zero_when_collection_is_empty()
{ {

@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.Framework
{ {
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger)); Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), TestLogger));
Mocker.SetConstant<IDroneServicesRequestBuilder>(new DroneServicesHttpRequestBuilder()); Mocker.SetConstant<ISonarrCloudRequestBuilder>(new SonarrCloudRequestBuilder());
} }
} }

@ -15,13 +15,13 @@ namespace NzbDrone.Core.DataAugmentation.DailySeries
public class DailySeriesDataProxy : IDailySeriesDataProxy public class DailySeriesDataProxy : IDailySeriesDataProxy
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IDroneServicesRequestBuilder _requestBuilder; private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly Logger _logger; private readonly Logger _logger;
public DailySeriesDataProxy(IHttpClient httpClient, IDroneServicesRequestBuilder requestBuilder, Logger logger) public DailySeriesDataProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_requestBuilder = requestBuilder; _requestBuilder = requestBuilder.Services;
_logger = logger; _logger = logger;
} }
@ -29,7 +29,10 @@ namespace NzbDrone.Core.DataAugmentation.DailySeries
{ {
try try
{ {
var dailySeriesRequest = _requestBuilder.Build("dailyseries"); var dailySeriesRequest = _requestBuilder.Create()
.Resource("/dailyseries")
.Build();
var response = _httpClient.Get<List<DailySeries>>(dailySeriesRequest); var response = _httpClient.Get<List<DailySeries>>(dailySeriesRequest);
return response.Resource.Select(c => c.TvdbId); return response.Resource.Select(c => c.TvdbId);
} }

@ -12,17 +12,20 @@ namespace NzbDrone.Core.DataAugmentation.Scene
public class SceneMappingProxy : ISceneMappingProxy public class SceneMappingProxy : ISceneMappingProxy
{ {
private readonly IHttpClient _httpClient; 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; _httpClient = httpClient;
_requestBuilder = requestBuilder; _requestBuilder = requestBuilder.Services;
} }
public List<SceneMapping> Fetch() public List<SceneMapping> Fetch()
{ {
var request = _requestBuilder.Build("/scenemapping"); var request = _requestBuilder.Create()
.Resource("/scenemapping")
.Build();
return _httpClient.Get<List<SceneMapping>>(request).Resource; return _httpClient.Get<List<SceneMapping>>(request).Resource;
} }
} }

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DataAugmentation.Xem.Model; using NzbDrone.Core.DataAugmentation.Xem.Model;
@ -18,32 +19,32 @@ namespace NzbDrone.Core.DataAugmentation.Xem
public class XemProxy : IXemProxy public class XemProxy : IXemProxy
{ {
private const string ROOT_URL = "http://thexem.de/map/";
private readonly Logger _logger; private readonly Logger _logger;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IHttpRequestBuilderFactory _xemRequestBuilder;
private const string XEM_BASE_URL = "http://thexem.de/map/";
private static readonly string[] IgnoredErrors = { "no single connection", "no show with the tvdb_id" }; 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; _httpClient = httpClient;
_logger = logger;
_xemRequestBuilder = new HttpRequestBuilder(XEM_BASE_URL) _xemRequestBuilder = new HttpRequestBuilder(ROOT_URL)
{ .AddSuffixQueryParam("origin", "tvdb")
PostProcess = r => r.UriBuilder.SetQueryParam("origin", "tvdb") .CreateFactory();
};
} }
public List<int> GetXemSeriesIds() public List<int> GetXemSeriesIds()
{ {
_logger.Debug("Fetching Series IDs from"); _logger.Debug("Fetching Series IDs from");
var request = _xemRequestBuilder.Build("/havemap"); var request = _xemRequestBuilder.Create()
.Resource("/havemap")
.Build();
var response = _httpClient.Get<XemResult<List<string>>>(request).Resource; var response = _httpClient.Get<XemResult<List<string>>>(request).Resource;
CheckForFailureResult(response); CheckForFailureResult(response);
@ -60,9 +61,10 @@ namespace NzbDrone.Core.DataAugmentation.Xem
{ {
_logger.Debug("Fetching Mappings for: {0}", id); _logger.Debug("Fetching Mappings for: {0}", id);
var request = _xemRequestBuilder.Create()
var request = _xemRequestBuilder.Build("/all"); .Resource("/all")
request.UriBuilder.SetQueryParam("id", id); .AddQueryParam("id", id)
.Build();
var response = _httpClient.Get<XemResult<List<XemSceneTvdbMapping>>>(request).Resource; var response = _httpClient.Get<XemResult<List<XemSceneTvdbMapping>>>(request).Resource;
@ -73,8 +75,10 @@ namespace NzbDrone.Core.DataAugmentation.Xem
{ {
_logger.Debug("Fetching alternate names"); _logger.Debug("Fetching alternate names");
var request = _xemRequestBuilder.Build("/allNames"); var request = _xemRequestBuilder.Create()
request.UriBuilder.SetQueryParam("seasonNumbers", true); .Resource("/allNames")
.AddQueryParam("seasonNumbers", true)
.Build();
var response = _httpClient.Get<XemResult<Dictionary<int, List<JObject>>>>(request).Resource; var response = _httpClient.Get<XemResult<Dictionary<int, List<JObject>>>>(request).Resource;

@ -107,7 +107,7 @@ namespace NzbDrone.Core.Download
if (response.StatusCode == HttpStatusCode.SeeOther || response.StatusCode == HttpStatusCode.Found) 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); _logger.Trace("Torrent request is being redirected to: {0}", locationHeader);

@ -63,11 +63,6 @@ namespace NzbDrone.Core
return dateTime.ToShortDateString(); 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<int> ints) public static int MaxOrDefault(this IEnumerable<int> ints)
{ {
if (ints == null) if (ints == null)

@ -13,9 +13,9 @@ namespace NzbDrone.Core.Http
{ {
// torcache behaves strangely when it has query params and/or no Referer or browser User-Agent. // 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. // 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/"); request.Headers.Add("Referer", request.Url.Scheme + @"://torcache.net/");
} }

@ -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); 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; yield return request;
} }

@ -172,14 +172,15 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
parameters = new BroadcastheNetTorrentQuery(); parameters = new BroadcastheNetTorrentQuery();
} }
var builder = new JsonRpcRequestBuilder(Settings.BaseUrl, "getTorrents", new object[] { Settings.ApiKey, parameters, PageSize, 0 }); var builder = new JsonRpcRequestBuilder(Settings.BaseUrl)
builder.SupressHttpError = true; .Call("getTorrents", Settings.ApiKey, parameters, PageSize, 0);
builder.SuppressHttpError = true;
for (var page = 0; page < maxPages;page++) 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());
} }
} }
} }

@ -114,8 +114,9 @@ namespace NzbDrone.Core.Indexers.HDBits
private IEnumerable<IndexerRequest> GetRequest(TorrentQuery query) private IEnumerable<IndexerRequest> GetRequest(TorrentQuery query)
{ {
var builder = new HttpRequestBuilder(Settings.BaseUrl); var request = new HttpRequestBuilder(Settings.BaseUrl)
var request = builder.Build("/api/torrents"); .Resource("/api/torrents")
.Build();
request.Method = HttpMethod.POST; request.Method = HttpMethod.POST;
const string appJson = "application/json"; const string appJson = "application/json";
@ -125,7 +126,7 @@ namespace NzbDrone.Core.Indexers.HDBits
query.Username = Settings.Username; query.Username = Settings.Username;
query.Passkey = Settings.ApiKey; query.Passkey = Settings.ApiKey;
request.Body = query.ToJson(); request.SetContent(query.ToJson());
yield return new IndexerRequest(request); yield return new IndexerRequest(request);
} }

@ -137,7 +137,7 @@ namespace NzbDrone.Core.Indexers
foreach (var request in pageableRequest) foreach (var request in pageableRequest)
{ {
url = request.Url.ToString(); url = request.Url.AbsoluteUri;
var page = FetchPage(request, parser); var page = FetchPage(request, parser);

@ -27,7 +27,7 @@ namespace NzbDrone.Core.Indexers.Newznab
throw new ApiKeyException("Invalid API key"); 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"); throw new ApiKeyException("Indexer requires an API key");
} }

@ -75,30 +75,34 @@ namespace NzbDrone.Core.Indexers.Rarbg
private IEnumerable<IndexerRequest> GetPagedRequests(string mode, int? tvdbId, string query, params object[] args) private IEnumerable<IndexerRequest> 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); var httpRequest = new HttpRequest(Settings.BaseUrl + "/pubapi_v2.php", HttpAccept.Json);
httpRequest.AddQueryParam("mode", mode); requestBuilder.AddQueryParam("mode", mode);
if (tvdbId.HasValue) if (tvdbId.HasValue)
{ {
httpRequest.AddQueryParam("search_tvdb", tvdbId.Value.ToString()); requestBuilder.AddQueryParam("search_tvdb", tvdbId.Value);
} }
if (query.IsNotNullOrWhiteSpace()) if (query.IsNotNullOrWhiteSpace())
{ {
httpRequest.AddQueryParam("search_string", string.Format(query, args)); requestBuilder.AddQueryParam("search_string", string.Format(query, args));
} }
if (!Settings.RankedOnly) if (!Settings.RankedOnly)
{ {
httpRequest.AddQueryParam("ranked", "0"); requestBuilder.AddQueryParam("ranked", "0");
} }
httpRequest.AddQueryParam("category", "18;41"); requestBuilder.AddQueryParam("category", "18;41");
httpRequest.AddQueryParam("limit", "100"); requestBuilder.AddQueryParam("limit", "100");
httpRequest.AddQueryParam("token", _tokenProvider.GetToken(Settings)); requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings));
httpRequest.AddQueryParam("format", "json_extended"); requestBuilder.AddQueryParam("format", "json_extended");
httpRequest.AddQueryParam("app_id", "Sonarr"); requestBuilder.AddQueryParam("app_id", "Sonarr");
yield return new IndexerRequest(httpRequest); yield return new IndexerRequest(httpRequest);
} }

@ -266,18 +266,18 @@ namespace NzbDrone.Core.Indexers
try 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) 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; return null;
} }
} }

@ -50,7 +50,10 @@ namespace NzbDrone.Core.Indexers.TorrentRss
if (Settings.Cookie.IsNotNullOrWhiteSpace()) 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; yield return request;

@ -23,7 +23,7 @@ namespace NzbDrone.Core.Indexers.Torznab
if (code >= 100 && code <= 199) throw new ApiKeyException("Invalid API key"); 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"); throw new ApiKeyException("Indexer requires an API key");
} }

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using NLog; using NLog;
using NzbDrone.Common.Cloud;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
@ -16,20 +17,23 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly Logger _logger; 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; _httpClient = httpClient;
_requestBuilder = requestBuilder.SkyHookTvdb;
_logger = logger; _logger = logger;
_requestBuilder = new HttpRequestBuilder("http://skyhook.sonarr.tv/v1/tvdb/{route}/en/");
} }
public Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId) public Tuple<Series, List<Episode>> GetSeriesInfo(int tvdbSeriesId)
{ {
var httpRequest = _requestBuilder.Build(tvdbSeriesId.ToString()); var httpRequest = _requestBuilder.Create()
httpRequest.AddSegment("route", "shows"); .SetSegment("route", "shows")
.Resource(tvdbSeriesId.ToString())
.Build();
httpRequest.AllowAutoRedirect = true; httpRequest.AllowAutoRedirect = true;
httpRequest.SuppressHttpError = true; httpRequest.SuppressHttpError = true;
@ -81,9 +85,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
} }
var term = System.Web.HttpUtility.UrlEncode((title.ToLower().Trim())); var term = System.Web.HttpUtility.UrlEncode((title.ToLower().Trim()));
var httpRequest = _requestBuilder.Build("?term={term}"); var httpRequest = _requestBuilder.Create()
httpRequest.AddSegment("route", "search"); .SetSegment("route", "search")
httpRequest.AddSegment("term", term); .AddQueryParam("term", title.ToLower().Trim())
.Build();
var httpResponse = _httpClient.Get<List<ShowResource>>(httpRequest); var httpResponse = _httpClient.Get<List<ShowResource>>(httpRequest);

@ -21,15 +21,14 @@ namespace NzbDrone.Core.Notifications.MediaBrowser
{ {
var path = "/Notifications/Admin"; var path = "/Notifications/Admin";
var request = BuildRequest(path, settings); var request = BuildRequest(path, settings);
request.Headers.ContentType = "application/json";
request.Body = new request.SetContent(new
{ {
Name = title, Name = title,
Description = message, Description = message,
ImageUrl = "https://raw.github.com/NzbDrone/NzbDrone/develop/Logo/64.png" ImageUrl = "https://raw.github.com/NzbDrone/NzbDrone/develop/Logo/64.png"
}.ToJson(); }.ToJson());
request.Headers.ContentType = "application/json";
ProcessRequest(request, settings); ProcessRequest(request, settings);
} }
@ -58,7 +57,7 @@ namespace NzbDrone.Core.Notifications.MediaBrowser
{ {
var url = string.Format(@"http://{0}/mediabrowser", settings.Address); 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) private void CheckForError(HttpResponse response)

@ -15,20 +15,22 @@ namespace NzbDrone.Core.Update
public class UpdatePackageProvider : IUpdatePackageProvider public class UpdatePackageProvider : IUpdatePackageProvider
{ {
private readonly IHttpClient _httpClient; 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; _httpClient = httpClient;
_requestBuilder = requestBuilder; _requestBuilder = requestBuilder.Services;
} }
public UpdatePackage GetLatestUpdate(string branch, Version currentVersion) public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
{ {
var request = _requestBuilder.Build("/update/{branch}"); var request = _requestBuilder.Create()
request.UriBuilder.SetQueryParam("version", currentVersion); .Resource("/update/{branch}")
request.UriBuilder.SetQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()); .AddQueryParam("version", currentVersion)
request.AddSegment("branch", branch); .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.SetSegment("branch", branch)
.Build();
var update = _httpClient.Get<UpdatePackageAvailable>(request).Resource; var update = _httpClient.Get<UpdatePackageAvailable>(request).Resource;
@ -39,10 +41,12 @@ namespace NzbDrone.Core.Update
public List<UpdatePackage> GetRecentUpdates(string branch, Version currentVersion) public List<UpdatePackage> GetRecentUpdates(string branch, Version currentVersion)
{ {
var request = _requestBuilder.Build("/update/{branch}/changes"); var request = _requestBuilder.Create()
request.UriBuilder.SetQueryParam("version", currentVersion); .Resource("/update/{branch}/changes")
request.UriBuilder.SetQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()); .AddQueryParam("version", currentVersion)
request.AddSegment("branch", branch); .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.SetSegment("branch", branch)
.Build();
var updates = _httpClient.Get<List<UpdatePackage>>(request); var updates = _httpClient.Get<List<UpdatePackage>>(request);

Loading…
Cancel
Save