Cardigann Auth first pass

pull/7/head
Qstick 3 years ago
parent cd65729239
commit a41ae141cd

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace NzbDrone.Common.Http
{
public static class CookieUtil
{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
// NOTE: we are not checking non-ascii characters and we should
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
public static Dictionary<string, string> CookieHeaderToDictionary(string cookieHeader)
{
var cookieDictionary = new Dictionary<string, string>();
if (cookieHeader == null)
{
return cookieDictionary;
}
var matches = _CookieRegex.Match(cookieHeader);
while (matches.Success)
{
if (matches.Groups.Count > 2)
{
cookieDictionary[matches.Groups[1].Value] = matches.Groups[2].Value;
}
matches = matches.NextMatch();
}
return cookieDictionary;
}
public static string CookieDictionaryToHeader(Dictionary<string, string> cookieDictionary)
{
if (cookieDictionary == null)
{
return "";
}
foreach (var kv in cookieDictionary)
{
if (kv.Key.IndexOfAny(InvalidKeyChars) > -1 || kv.Value.IndexOfAny(InvalidValueChars) > -1)
{
throw new FormatException($"The cookie '{kv.Key}={kv.Value}' is malformed.");
}
}
return string.Join("; ", cookieDictionary.Select(kv => kv.Key + "=" + kv.Value));
}
}
}

@ -143,7 +143,7 @@ namespace NzbDrone.Common.Http.Dispatchers
}
}
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), httpWebResponse.Cookies, data, httpWebResponse.StatusCode);
}
public void DownloadFile(string url, string fileName)

@ -11,18 +11,20 @@ namespace NzbDrone.Common.Http
{
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, CookieCollection cookies, byte[] binaryData, HttpStatusCode statusCode = HttpStatusCode.OK)
{
Request = request;
Headers = headers;
Cookies = cookies;
ResponseData = binaryData;
StatusCode = statusCode;
}
public HttpResponse(HttpRequest request, HttpHeader headers, string content, HttpStatusCode statusCode = HttpStatusCode.OK)
public HttpResponse(HttpRequest request, HttpHeader headers, CookieCollection cookies, string content, HttpStatusCode statusCode = HttpStatusCode.OK)
{
Request = request;
Headers = headers;
Cookies = cookies;
ResponseData = Headers.GetEncodingFromContentType().GetBytes(content);
_content = content;
StatusCode = statusCode;
@ -30,6 +32,7 @@ namespace NzbDrone.Common.Http
public HttpRequest Request { get; private set; }
public HttpHeader Headers { get; private set; }
public CookieCollection Cookies { get; private set; }
public HttpStatusCode StatusCode { get; private set; }
public byte[] ResponseData { get; private set; }
@ -65,14 +68,9 @@ namespace NzbDrone.Common.Http
{
var result = new Dictionary<string, string>();
var setCookieHeaders = GetCookieHeaders();
foreach (var cookie in setCookieHeaders)
foreach (Cookie cookie in Cookies)
{
var match = RegexSetCookie.Match(cookie);
if (match.Success)
{
result[match.Groups[1].Value] = match.Groups[2].Value;
}
result[cookie.Name] = cookie.Value;
}
return result;
@ -95,7 +93,7 @@ namespace NzbDrone.Common.Http
where T : new()
{
public HttpResponse(HttpResponse response)
: base(response.Request, response.Headers, response.ResponseData, response.StatusCode)
: base(response.Request, response.Headers, response.Cookies, response.ResponseData, response.StatusCode)
{
Resource = Json.Deserialize<T>(response.Content);
}

@ -1,4 +1,5 @@
using System;
using System.Net;
using System.Text;
using Moq;
using NUnit.Framework;
@ -31,7 +32,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), Encoding.ASCII.GetBytes(json)));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.ASCII.GetBytes(json)));
}
[Test]

@ -1,4 +1,5 @@
using System.Linq;
using System.Linq;
using System.Net;
using System.Text;
using FluentAssertions;
using NUnit.Framework;
@ -40,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests
private IndexerResponse CreateResponse(string url, string content)
{
var httpRequest = new HttpRequest(url);
var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), Encoding.UTF8.GetBytes(content));
var httpResponse = new HttpResponse(httpRequest, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(content));
return new IndexerResponse(new IndexerRequest(httpRequest), httpResponse);
}

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -32,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } }).Releases;

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using System.Text;
using FluentAssertions;
using Moq;
@ -44,7 +45,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), responseJson));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), responseJson));
var torrents = Subject.Fetch(_movieSearchCriteria).Releases;
@ -73,7 +74,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
Mocker.GetMock<IHttpClient>()
.Setup(v => v.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), Encoding.UTF8.GetBytes(responseJson)));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), Encoding.UTF8.GetBytes(responseJson)));
var torrents = Subject.Fetch(_movieSearchCriteria).Releases;

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -90,7 +91,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } }).Releases;

@ -1,4 +1,5 @@
using System;
using System.Net;
using System.Xml;
using FluentAssertions;
using Moq;
@ -30,7 +31,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), caps));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), caps));
}
[Test]

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -42,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 }, Limit = 100, Offset = 0 }).Releases;

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -32,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria()).Releases;

@ -1,4 +1,5 @@
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -36,11 +37,11 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), authStream.ToString()));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), authStream.ToString()));
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, responseJson));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, new CookieCollection(), responseJson));
var torrents = Subject.Fetch(new MovieSearchCriteria()).Releases;

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -37,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } }).Releases;
@ -64,7 +65,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 20, error: \"some message\" }"));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 20, error: \"some message\" }"));
var releases = Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } }).Releases;
@ -76,7 +77,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 25, error: \"some message\" }"));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), "{ error_code: 25, error: \"some message\" }"));
var releases = Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } }).Releases;

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -37,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
}
[Test]

@ -1,3 +1,4 @@
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -26,7 +27,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorrentRssIndexerTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
}
[Test]

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@ -43,7 +44,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria()).Releases;
@ -72,7 +73,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria()).Releases;
@ -102,7 +103,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new CookieCollection(), recentFeed));
var releases = Subject.Fetch(new MovieSearchCriteria()).Releases;

@ -24,7 +24,10 @@ namespace NzbDrone.Core.Indexers.Cardigann
{
return new CardigannRequestGenerator(_definitionService.GetDefinition(Settings.DefinitionFile),
Settings,
_logger);
_logger)
{
HttpClient = _httpClient
};
}
public override IParseIndexerResponse GetParser()

@ -222,7 +222,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
_logger.Debug($"{name} got value {value.ToJson()}");
if (setting.Type == "text")
if (setting.Type == "text" || setting.Type == "password")
{
variables[name] = value;
}
@ -232,13 +232,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
else if (setting.Type == "select")
{
_logger.Debug($"setting options: {setting.Options.ToJson()}");
_logger.Debug($"Setting options: {setting.Options.ToJson()}");
var sorted = setting.Options.OrderBy(x => x.Key).ToList();
var selected = sorted[(int)(long)value];
_logger.Debug($"selected option: {selected.ToJson()}");
_logger.Debug($"Selected option: {selected.ToJson()}");
variables[name] = selected.Value;
variables[name] = selected.Key;
}
else if (setting.Type == "info")
{
@ -249,7 +249,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
throw new NotSupportedException();
}
_logger.Debug($"Setting {setting.Name} to {variables[name]}");
_logger.Debug($"Setting {setting.Name} to {(setting.Type == "password" ? "Redacted" : variables[name])}");
}
return variables;

@ -0,0 +1,24 @@
using NzbDrone.Common.Exceptions;
using NzbDrone.Core.Indexers.Cardigann;
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
{
public class CardigannConfigException : NzbDroneException
{
private readonly CardigannDefinition _configuration;
public CardigannConfigException(CardigannDefinition config, string message, params object[] args)
: base(message, args)
{
_configuration = config;
}
public CardigannConfigException(CardigannDefinition config, string message)
: base(message)
{
_configuration = config;
}
public CardigannDefinition Configuration => _configuration;
}
}

@ -333,8 +333,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
continue;
}
_logger.Error("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value == null ? "<null>" : value, ex.Message);
throw;
//Parser errors usually happen on every result and are costly to performance, trace only
_logger.Trace("Error while parsing field={0}, selector={1}, value={2}: {3}", field.Key, field.Value.Selector, value == null ? "<null>" : value, ex.Message);
}
}

@ -1,15 +1,25 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Definitions.Cardigann;
using NzbDrone.Core.IndexerSearch.Definitions;
namespace NzbDrone.Core.Indexers.Cardigann
{
public class CardigannRequestGenerator : CardigannBase, IIndexerRequestGenerator
{
public IHttpClient HttpClient { get; set; }
public IDictionary<string, string> Cookies { get; set; }
protected HttpResponse landingResult;
protected IHtmlDocument landingResultDocument;
public CardigannRequestGenerator(CardigannDefinition definition,
CardigannSettings settings,
Logger logger)
@ -144,8 +154,617 @@ namespace NzbDrone.Core.Indexers.Cardigann
return variables;
}
private void Authenticate()
{
var login = _definition.Login;
if (login == null || TestLogin())
{
return;
}
if (login.Method == "post")
{
var pairs = new Dictionary<string, string>();
foreach (var input in login.Inputs)
{
var value = ApplyGoTemplateText(input.Value);
pairs.Add(input.Key, value);
}
var loginUrl = ResolvePath(login.Path).ToString();
CookiesUpdater(null, null);
var requestBuilder = new HttpRequestBuilder(loginUrl)
{
LogResponseContent = true,
Method = HttpMethod.POST,
AllowAutoRedirect = true,
SuppressHttpError = true
};
requestBuilder.Headers.Add("Referer", SiteLink);
var response = HttpClient.Execute(requestBuilder.Build());
Cookies = response.GetCookies();
CheckForError(response, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
}
else if (login.Method == "form")
{
var loginUrl = ResolvePath(login.Path).ToString();
var queryCollection = new NameValueCollection();
var pairs = new Dictionary<string, string>();
var formSelector = login.Form;
if (formSelector == null)
{
formSelector = "form";
}
// landingResultDocument might not be initiated if the login is caused by a relogin during a query
if (landingResultDocument == null)
{
GetConfigurationForSetup(true);
}
var form = landingResultDocument.QuerySelector(formSelector);
if (form == null)
{
throw new CardigannConfigException(_definition, string.Format("Login failed: No form found on {0} using form selector {1}", loginUrl, formSelector));
}
var inputs = form.QuerySelectorAll("input");
if (inputs == null)
{
throw new CardigannConfigException(_definition, string.Format("Login failed: No inputs found on {0} using form selector {1}", loginUrl, formSelector));
}
var submitUrlstr = form.GetAttribute("action");
if (login.Submitpath != null)
{
submitUrlstr = login.Submitpath;
}
foreach (var input in inputs)
{
var name = input.GetAttribute("name");
if (name == null)
{
continue;
}
var value = input.GetAttribute("value");
if (value == null)
{
value = "";
}
pairs[name] = value;
}
foreach (var input in login.Inputs)
{
var value = ApplyGoTemplateText(input.Value);
var inputKey = input.Key;
if (login.Selectors)
{
var inputElement = landingResultDocument.QuerySelector(input.Key);
if (inputElement == null)
{
throw new CardigannConfigException(_definition, string.Format("Login failed: No input found using selector {0}", input.Key));
}
inputKey = inputElement.GetAttribute("name");
}
pairs[inputKey] = value;
}
// selector inputs
if (login.Selectorinputs != null)
{
foreach (var selectorinput in login.Selectorinputs)
{
string value = null;
try
{
value = HandleSelector(selectorinput.Value, landingResultDocument.FirstElementChild);
pairs[selectorinput.Key] = value;
}
catch (Exception ex)
{
throw new Exception(string.Format("Error while parsing selector input={0}, selector={1}, value={2}: {3}", selectorinput.Key, selectorinput.Value.Selector, value, ex.Message));
}
}
}
// getselector inputs
if (login.Getselectorinputs != null)
{
foreach (var selectorinput in login.Getselectorinputs)
{
string value = null;
try
{
value = HandleSelector(selectorinput.Value, landingResultDocument.FirstElementChild);
queryCollection[selectorinput.Key] = value;
}
catch (Exception ex)
{
throw new Exception(string.Format("Error while parsing get selector input={0}, selector={1}, value={2}: {3}", selectorinput.Key, selectorinput.Value.Selector, value, ex.Message));
}
}
}
if (queryCollection.Count > 0)
{
submitUrlstr += "?" + queryCollection.GetQueryString();
}
var submitUrl = ResolvePath(submitUrlstr, new Uri(loginUrl));
// automatically solve simpleCaptchas, if used
var simpleCaptchaPresent = landingResultDocument.QuerySelector("script[src*=\"simpleCaptcha\"]");
if (simpleCaptchaPresent != null)
{
var captchaUrl = ResolvePath("simpleCaptcha.php?numImages=1");
var requestBuilder = new HttpRequestBuilder(captchaUrl.ToString())
{
LogResponseContent = true,
Method = HttpMethod.GET
};
requestBuilder.Headers.Add("Referer", loginUrl);
var simpleCaptchaResult = HttpClient.Execute(requestBuilder.Build());
var simpleCaptchaJSON = JObject.Parse(simpleCaptchaResult.Content);
var captchaSelection = simpleCaptchaJSON["images"][0]["hash"].ToString();
pairs["captchaSelection"] = captchaSelection;
pairs["submitme"] = "X";
}
if (login.Captcha != null)
{
var captcha = login.Captcha;
if (captcha.Type == "image")
{
_settings.ExtraFieldData.TryGetValue("CaptchaText", out var captchaText);
if (captchaText != null)
{
var input = captcha.Input;
if (login.Selectors)
{
var inputElement = landingResultDocument.QuerySelector(captcha.Input);
if (inputElement == null)
{
throw new CardigannConfigException(_definition, string.Format("Login failed: No captcha input found using {0}", captcha.Input));
}
input = inputElement.GetAttribute("name");
}
pairs[input] = (string)captchaText;
}
}
if (captcha.Type == "text")
{
_settings.ExtraFieldData.TryGetValue("CaptchaAnswer", out var captchaAnswer);
if (captchaAnswer != null)
{
var input = captcha.Input;
if (login.Selectors)
{
var inputElement = landingResultDocument.QuerySelector(captcha.Input);
if (inputElement == null)
{
throw new CardigannConfigException(_definition, string.Format("Login failed: No captcha input found using {0}", captcha.Input));
}
input = inputElement.GetAttribute("name");
}
pairs[input] = (string)captchaAnswer;
}
}
}
// clear landingResults/Document, otherwise we might use an old version for a new relogin (if GetConfigurationForSetup() wasn't called before)
landingResult = null;
landingResultDocument = null;
HttpResponse loginResult = null;
var enctype = form.GetAttribute("enctype");
if (enctype == "multipart/form-data")
{
var headers = new Dictionary<string, string>();
var boundary = "---------------------------" + DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString().Replace(".", "");
var bodyParts = new List<string>();
foreach (var pair in pairs)
{
var part = "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"" + pair.Key + "\"\r\n" +
"\r\n" +
pair.Value;
bodyParts.Add(part);
}
bodyParts.Add("--" + boundary + "--");
headers.Add("Content-Type", "multipart/form-data; boundary=" + boundary);
var body = string.Join("\r\n", bodyParts);
var requestBuilder = new HttpRequestBuilder(submitUrl.ToString())
{
LogResponseContent = true,
Method = HttpMethod.POST,
AllowAutoRedirect = true
};
requestBuilder.Headers.Add("Referer", SiteLink);
requestBuilder.SetCookies(Cookies);
foreach (var pair in pairs)
{
requestBuilder.AddFormParameter(pair.Key, pair.Value);
}
foreach (var header in headers)
{
requestBuilder.SetHeader(header.Key, header.Value);
}
var request = requestBuilder.Build();
request.SetContent(body);
loginResult = HttpClient.Execute(request);
}
else
{
var requestBuilder = new HttpRequestBuilder(submitUrl.ToString())
{
LogResponseContent = true,
Method = HttpMethod.POST,
AllowAutoRedirect = true,
SuppressHttpError = true
};
requestBuilder.SetCookies(Cookies);
requestBuilder.Headers.Add("Referer", loginUrl);
foreach (var pair in pairs)
{
requestBuilder.AddFormParameter(pair.Key, pair.Value);
}
loginResult = HttpClient.Execute(requestBuilder.Build());
}
Cookies = loginResult.GetCookies();
CheckForError(loginResult, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
}
else if (login.Method == "cookie")
{
CookiesUpdater(null, null);
_settings.ExtraFieldData.TryGetValue("cookie", out var cookies);
CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now + TimeSpan.FromDays(30));
}
else if (login.Method == "get")
{
var queryCollection = new NameValueCollection();
foreach (var input in login.Inputs)
{
var value = ApplyGoTemplateText(input.Value);
queryCollection.Add(input.Key, value);
}
var loginUrl = ResolvePath(login.Path + "?" + queryCollection.GetQueryString()).ToString();
CookiesUpdater(null, null);
var requestBuilder = new HttpRequestBuilder(loginUrl)
{
LogResponseContent = true,
Method = HttpMethod.GET,
SuppressHttpError = true
};
requestBuilder.Headers.Add("Referer", SiteLink);
var response = HttpClient.Execute(requestBuilder.Build());
Cookies = response.GetCookies();
CheckForError(response, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
}
else if (login.Method == "oneurl")
{
var oneUrl = ApplyGoTemplateText(login.Inputs["oneurl"]);
var loginUrl = ResolvePath(login.Path + oneUrl).ToString();
CookiesUpdater(null, null);
var requestBuilder = new HttpRequestBuilder(loginUrl)
{
LogResponseContent = true,
Method = HttpMethod.GET,
SuppressHttpError = true
};
requestBuilder.Headers.Add("Referer", SiteLink);
var response = HttpClient.Execute(requestBuilder.Build());
Cookies = response.GetCookies();
CheckForError(response, login.Error);
CookiesUpdater(Cookies, DateTime.Now + TimeSpan.FromDays(30));
}
else
{
throw new NotImplementedException("Login method " + login.Method + " not implemented");
}
}
protected bool CheckForError(HttpResponse loginResult, IList<ErrorBlock> errorBlocks)
{
if (loginResult.StatusCode == HttpStatusCode.Unauthorized)
{
throw new HttpException(loginResult);
}
if (errorBlocks == null)
{
return true;
}
var resultParser = new HtmlParser();
var resultDocument = resultParser.ParseDocument(loginResult.Content);
foreach (var error in errorBlocks)
{
var selection = resultDocument.QuerySelector(error.Selector);
if (selection != null)
{
var errorMessage = selection.TextContent;
if (error.Message != null)
{
errorMessage = HandleSelector(error.Message, resultDocument.FirstElementChild);
}
throw new CardigannConfigException(_definition, string.Format("Error: {0}", errorMessage.Trim()));
}
}
return true;
}
public void GetConfigurationForSetup(bool automaticlogin)
{
var login = _definition.Login;
if (login == null || login.Method != "form")
{
return;
}
var loginUrl = ResolvePath(login.Path);
Cookies = null;
if (login.Cookies != null)
{
Cookies = CookieUtil.CookieHeaderToDictionary(string.Join("; ", login.Cookies));
}
var requestBuilder = new HttpRequestBuilder(loginUrl.AbsoluteUri)
{
LogResponseContent = true,
Method = HttpMethod.GET
};
requestBuilder.Headers.Add("Referer", SiteLink);
if (Cookies != null)
{
requestBuilder.SetCookies(Cookies);
}
landingResult = HttpClient.Execute(requestBuilder.Build());
Cookies = landingResult.GetCookies();
// Some sites have a temporary redirect before the login page, we need to process it.
//if (_definition.Followredirect)
//{
// await FollowIfRedirect(landingResult, loginUrl.AbsoluteUri, overrideCookies: landingResult.Cookies, accumulateCookies: true);
//}
var hasCaptcha = false;
var htmlParser = new HtmlParser();
landingResultDocument = htmlParser.ParseDocument(landingResult.Content);
if (login.Captcha != null)
{
var captcha = login.Captcha;
if (captcha.Type == "image")
{
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
if (captchaElement != null)
{
hasCaptcha = true;
//TODO Bubble this to UI when we get a captcha so that user can action it
//Jackett does this by inserting image or question into the extrasettings which then show up in the add modal
//
//var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl);
//var captchaImageData = RequestWithCookiesAsync(captchaUrl.ToString(), landingResult.GetCookies, referer: loginUrl.AbsoluteUri);
// var CaptchaImage = new ImageItem { Name = "Captcha Image" };
//var CaptchaText = new StringItem { Name = "Captcha Text" };
//CaptchaImage.Value = captchaImageData.ContentBytes;
//configData.AddDynamic("CaptchaImage", CaptchaImage);
//configData.AddDynamic("CaptchaText", CaptchaText);
}
else
{
_logger.Debug(string.Format("CardigannIndexer ({0}): No captcha image found", _definition.Id));
}
}
else if (captcha.Type == "text")
{
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
if (captchaElement != null)
{
hasCaptcha = true;
//var captchaChallenge = new DisplayItem(captchaElement.TextContent) { Name = "Captcha Challenge" };
//var captchaAnswer = new StringItem { Name = "Captcha Answer" };
//configData.AddDynamic("CaptchaChallenge", captchaChallenge);
//configData.AddDynamic("CaptchaAnswer", captchaAnswer);
}
else
{
_logger.Debug(string.Format("CardigannIndexer ({0}): No captcha image found", _definition.Id));
}
}
else
{
throw new NotImplementedException(string.Format("Captcha type \"{0}\" is not implemented", captcha.Type));
}
}
if (hasCaptcha && automaticlogin)
{
_logger.Error(string.Format("CardigannIndexer ({0}): Found captcha during automatic login, aborting", _definition.Id));
return;
}
return;
}
protected bool TestLogin()
{
var login = _definition.Login;
if (login == null || login.Test == null)
{
return false;
}
// test if login was successful
var loginTestUrl = ResolvePath(login.Test.Path).ToString();
// var headers = ParseCustomHeaders(_definition.Search?.Headers, GetBaseTemplateVariables());
var requestBuilder = new HttpRequestBuilder(loginTestUrl)
{
LogResponseContent = true,
Method = HttpMethod.GET,
SuppressHttpError = true
};
if (Cookies != null)
{
requestBuilder.SetCookies(Cookies);
}
var testResult = HttpClient.Execute(requestBuilder.Build());
if (testResult.HasHttpRedirect)
{
var errormessage = "Login Failed, got redirected.";
var domainHint = GetRedirectDomainHint(testResult);
if (domainHint != null)
{
errormessage += " Try changing the indexer URL to " + domainHint + ".";
}
_logger.Debug(errormessage);
return false;
}
if (login.Test.Selector != null)
{
var testResultParser = new HtmlParser();
var testResultDocument = testResultParser.ParseDocument(testResult.Content);
var selection = testResultDocument.QuerySelectorAll(login.Test.Selector);
if (selection.Length == 0)
{
_logger.Debug(string.Format("Login failed: Selector \"{0}\" didn't match", login.Test.Selector));
return false;
}
}
return true;
}
protected string GetRedirectDomainHint(string requestUrl, string redirectUrl)
{
if (requestUrl.StartsWith(SiteLink) && !redirectUrl.StartsWith(SiteLink))
{
var uri = new Uri(redirectUrl);
return uri.Scheme + "://" + uri.Host + "/";
}
return null;
}
protected string GetRedirectDomainHint(HttpResponse result) => GetRedirectDomainHint(result.Request.Url.ToString(), result.Headers.GetSingleValue("Location"));
protected bool CheckIfLoginIsNeeded(HttpResponse response, IHtmlDocument document)
{
if (response.HasHttpRedirect)
{
var domainHint = GetRedirectDomainHint(response);
if (domainHint != null)
{
var errormessage = "Got redirected to another domain. Try changing the indexer URL to " + domainHint + ".";
throw new Exception(errormessage);
}
return true;
}
if (_definition.Login == null || _definition.Login.Test == null)
{
return false;
}
if (_definition.Login.Test.Selector != null)
{
var selection = document.QuerySelectorAll(_definition.Login.Test.Selector);
if (selection.Length == 0)
{
return true;
}
}
return false;
}
private IEnumerable<IndexerRequest> GetRequest(Dictionary<string, object> variables)
{
Cookies = GetCookies();
if (Cookies == null || !Cookies.Any())
{
Authenticate();
}
var search = _definition.Search;
var mappedCategories = MapTorznabCapsToTrackers((int[])variables[".Query.Categories"]);
@ -270,6 +889,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
}
}
if (Cookies != null)
{
foreach (var cookie in Cookies)
{
request.HttpRequest.Cookies.Add(cookie.Key, cookie.Value);
}
}
request.HttpRequest.Method = method;
yield return request;

@ -102,13 +102,17 @@ namespace NzbDrone.Core.Indexers.Cardigann
// http://www.codeproject.com/Articles/33298/C-Date-Time-Parser
public static DateTime FromFuzzyTime(string str, string format = null)
{
/*var dtFormat = format == "UK" ?
DateTimeRoutines.DateTimeRoutines.DateTimeFormat.UkDate :
DateTimeRoutines.DateTimeRoutines.DateTimeFormat.UsaDate;
if (DateTimeRoutines.DateTimeRoutines.TryParseDateOrTime(
str, dtFormat, out DateTimeRoutines.DateTimeRoutines.ParsedDateTime dt))
return dt.DateTime;*/
//var dtFormat = format == "UK" ?
// DateTimeRoutines.DateTimeRoutines.DateTimeFormat.UkDate :
// DateTimeRoutines.DateTimeRoutines.DateTimeFormat.UsaDate;
//if (DateTimeRoutines.DateTimeRoutines.TryParseDateOrTime(
// str, dtFormat, out DateTimeRoutines.DateTimeRoutines.ParsedDateTime dt))
// return dt.DateTime;
if (DateTime.TryParse(str, out var dateTimeParsed))
{
return dateTimeParsed;
}
throw new Exception("FromFuzzyTime parsing failed");
}

@ -12,7 +12,6 @@ using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers
{
@ -43,11 +42,12 @@ namespace NzbDrone.Core.Indexers
public override IndexerPageableQueryResult Fetch(MovieSearchCriteria searchCriteria)
{
//TODO: Re-Enable when All Indexer Caps are fixed and tests don't fail
//if (!SupportsSearch)
//{
// return new List<ReleaseInfo>();
// return new IndexerPageableQueryResult();
//}
return FetchReleases(g => g.GetSearchRequests(searchCriteria));
return FetchReleases(g => SetCookieFunctions(g).GetSearchRequests(searchCriteria));
}
public override IndexerPageableQueryResult Fetch(MusicSearchCriteria searchCriteria)
@ -57,7 +57,7 @@ namespace NzbDrone.Core.Indexers
return new IndexerPageableQueryResult();
}
return FetchReleases(g => g.GetSearchRequests(searchCriteria));
return FetchReleases(g => SetCookieFunctions(g).GetSearchRequests(searchCriteria));
}
public override IndexerPageableQueryResult Fetch(TvSearchCriteria searchCriteria)
@ -67,7 +67,7 @@ namespace NzbDrone.Core.Indexers
return new IndexerPageableQueryResult();
}
return FetchReleases(g => g.GetSearchRequests(searchCriteria));
return FetchReleases(g => SetCookieFunctions(g).GetSearchRequests(searchCriteria));
}
public override IndexerPageableQueryResult Fetch(BookSearchCriteria searchCriteria)
@ -77,7 +77,7 @@ namespace NzbDrone.Core.Indexers
return new IndexerPageableQueryResult();
}
return FetchReleases(g => g.GetSearchRequests(searchCriteria));
return FetchReleases(g => SetCookieFunctions(g).GetSearchRequests(searchCriteria));
}
public override IndexerPageableQueryResult Fetch(BasicSearchCriteria searchCriteria)
@ -87,13 +87,11 @@ namespace NzbDrone.Core.Indexers
return new IndexerPageableQueryResult();
}
return FetchReleases(g => g.GetSearchRequests(searchCriteria));
return FetchReleases(g => SetCookieFunctions(g).GetSearchRequests(searchCriteria));
}
protected IndexerPageableRequestChain GetRequestChain(SearchCriteriaBase searchCriteria = null)
protected IIndexerRequestGenerator SetCookieFunctions(IIndexerRequestGenerator generator)
{
var generator = GetRequestGenerator();
//A func ensures cookies are always updated to the latest. This way, the first page could update the cookies and then can be reused by the second page.
generator.GetCookies = () =>
{
@ -107,14 +105,12 @@ namespace NzbDrone.Core.Indexers
return cookies;
};
var requests = generator.GetSearchRequests(searchCriteria as MovieSearchCriteria);
generator.CookiesUpdater = (cookies, expiration) =>
{
_indexerStatusService.UpdateCookies(Definition.Id, cookies, expiration);
};
return requests;
return generator;
}
protected virtual IndexerPageableQueryResult FetchReleases(Func<IIndexerRequestGenerator, IndexerPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
@ -373,7 +369,11 @@ namespace NzbDrone.Core.Indexers
{
_indexerStatusService.UpdateCookies(Definition.Id, cookies, expiration);
};
var generator = GetRequestGenerator();
generator = SetCookieFunctions(generator);
var firstRequest = generator.GetSearchRequests(new BasicSearchCriteria { SearchType = "search" }).GetAllTiers().FirstOrDefault()?.FirstOrDefault();
if (firstRequest == null)

@ -32,12 +32,12 @@ namespace NzbDrone.Core.Indexers
public IDictionary<string, string> GetIndexerCookies(int indexerId)
{
return GetProviderStatus(indexerId).Cookies;
return GetProviderStatus(indexerId)?.Cookies ?? null;
}
public DateTime GetIndexerCookiesExpirationDate(int indexerId)
{
return GetProviderStatus(indexerId).CookiesExpirationDate ?? DateTime.Now + TimeSpan.FromDays(12);
return GetProviderStatus(indexerId)?.CookiesExpirationDate ?? DateTime.Now + TimeSpan.FromDays(12);
}
public void UpdateRssSyncStatus(int indexerId, ReleaseInfo releaseInfo)
@ -54,12 +54,15 @@ namespace NzbDrone.Core.Indexers
public void UpdateCookies(int indexerId, IDictionary<string, string> cookies, DateTime? expiration)
{
lock (_syncRoot)
if (indexerId > 0)
{
var status = GetProviderStatus(indexerId);
status.Cookies = cookies;
status.CookiesExpirationDate = expiration;
_providerStatusRepository.Upsert(status);
lock (_syncRoot)
{
var status = GetProviderStatus(indexerId);
status.Cookies = cookies;
status.CookiesExpirationDate = expiration;
_providerStatusRepository.Upsert(status);
}
}
}
}

@ -35,20 +35,16 @@ namespace Prowlarr.Api.V1.Indexers
if (definition.Implementation == typeof(Cardigann).Name)
{
Console.WriteLine("mapping cardigann def");
var extraFields = definition.ExtraFields?.Select((x, i) => MapField(x, i)).ToList() ?? new List<Field>();
resource.Fields.AddRange(extraFields);
var settings = (CardigannSettings)definition.Settings;
Console.WriteLine($"Got {settings.ExtraFieldData.Count} fields");
foreach (var setting in settings.ExtraFieldData)
{
var field = extraFields.FirstOrDefault(x => x.Name == setting.Key);
if (field != null)
{
Console.WriteLine($"setting {setting.Key} to {setting.Value}");
field.Value = setting.Value;
}
}
@ -79,8 +75,6 @@ namespace Prowlarr.Api.V1.Indexers
if (resource.Implementation == typeof(Cardigann).Name)
{
Console.WriteLine("mapping cardigann resource");
var standardFields = base.ToResource(definition).Fields.Select(x => x.Name).ToList();
var settings = (CardigannSettings)definition.Settings;
@ -105,7 +99,6 @@ namespace Prowlarr.Api.V1.Indexers
private Field MapField(SettingsField fieldAttribute, int order)
{
Console.WriteLine($"Adding field {fieldAttribute.Name}");
var field = new Field
{
Name = fieldAttribute.Name,

Loading…
Cancel
Save