New: Added CAPTCHA support to Rarbg.

pull/1421/head
Taloth Saldono 8 years ago
parent e9eab0ae48
commit 73fb216e6f

@ -81,7 +81,7 @@ namespace NzbDrone.Api.Extensions.Pipelines
{ {
if (request.Url.Query.IsNotNullOrWhiteSpace()) if (request.Url.Query.IsNotNullOrWhiteSpace())
{ {
return string.Concat(request.Url.Path, request.Url.Query); return string.Concat(request.Url.Path, "?", request.Url.Query);
} }
else else
{ {

@ -27,7 +27,7 @@ namespace NzbDrone.Api
Get["schema"] = x => GetTemplates(); Get["schema"] = x => GetTemplates();
Post["test"] = x => Test(ReadResourceFromRequest(true)); Post["test"] = x => Test(ReadResourceFromRequest(true));
Post["connectData/{stage}"] = x => ConnectData(x.stage, ReadResourceFromRequest(true)); Post["action/{action}"] = x => RequestAction(x.action, ReadResourceFromRequest(true));
GetResourceAll = GetAll; GetResourceAll = GetAll;
GetResourceById = GetProviderById; GetResourceById = GetProviderById;
@ -183,13 +183,13 @@ namespace NzbDrone.Api
} }
private Response ConnectData(string stage, TProviderResource providerResource) private Response RequestAction(string action, TProviderResource providerResource)
{ {
var providerDefinition = GetDefinition(providerResource, true, false); var providerDefinition = GetDefinition(providerResource, true, false);
if (!providerDefinition.Enable) return "{}"; var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString());
object data = _providerFactory.ConnectData(providerDefinition, stage, (IDictionary<string, object>) Request.Query.ToDictionary()); var data = _providerFactory.RequestAction(providerDefinition, action, query);
Response resp = JsonConvert.SerializeObject(data); Response resp = JsonConvert.SerializeObject(data);
resp.ContentType = "application/json"; resp.ContentType = "application/json";
return resp; return resp;

@ -104,7 +104,7 @@ namespace NzbDrone.Common.Http.Dispatchers
default: default:
throw new NotSupportedException(string.Format("HttpCurl method {0} not supported", request.Method)); throw new NotSupportedException(string.Format("HttpCurl method {0} not supported", request.Method));
} }
curlEasy.UserAgent = UserAgentBuilder.UserAgent; curlEasy.UserAgent = request.UseSimplifiedUserAgent ? UserAgentBuilder.UserAgentSimplified : UserAgentBuilder.UserAgent; ;
curlEasy.FollowLocation = request.AllowAutoRedirect; curlEasy.FollowLocation = request.AllowAutoRedirect;
if (request.RequestTimeout != TimeSpan.Zero) if (request.RequestTimeout != TimeSpan.Zero)

@ -26,7 +26,7 @@ namespace NzbDrone.Common.Http.Dispatchers
webRequest.AutomaticDecompression = DecompressionMethods.GZip; webRequest.AutomaticDecompression = DecompressionMethods.GZip;
webRequest.Method = request.Method.ToString(); webRequest.Method = request.Method.ToString();
webRequest.UserAgent = UserAgentBuilder.UserAgent; webRequest.UserAgent = request.UseSimplifiedUserAgent ? UserAgentBuilder.UserAgentSimplified : UserAgentBuilder.UserAgent;
webRequest.KeepAlive = request.ConnectionKeepAlive; webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = request.AllowAutoRedirect; webRequest.AllowAutoRedirect = request.AllowAutoRedirect;
webRequest.CookieContainer = cookies; webRequest.CookieContainer = cookies;

@ -34,6 +34,7 @@ namespace NzbDrone.Common.Http
public byte[] ContentData { get; set; } public byte[] ContentData { get; set; }
public string ContentSummary { get; set; } public string ContentSummary { get; set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }

@ -19,6 +19,7 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; } public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; } public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; } public bool SuppressHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; } public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; } public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; } public bool LogResponseContent { get; set; }
@ -99,6 +100,7 @@ namespace NzbDrone.Common.Http
{ {
request.Method = Method; request.Method = Method;
request.SuppressHttpError = SuppressHttpError; request.SuppressHttpError = SuppressHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect; request.AllowAutoRedirect = AllowAutoRedirect;
request.ConnectionKeepAlive = ConnectionKeepAlive; request.ConnectionKeepAlive = ConnectionKeepAlive;
request.LogResponseContent = LogResponseContent; request.LogResponseContent = LogResponseContent;

@ -6,12 +6,16 @@ namespace NzbDrone.Common.Http
public static class UserAgentBuilder public static class UserAgentBuilder
{ {
public static string UserAgent { get; private set; } public static string UserAgent { get; private set; }
public static string UserAgentSimplified { get; private set; }
static UserAgentBuilder() static UserAgentBuilder()
{ {
UserAgent = string.Format("Sonarr/{0} ({1} {2}) ", UserAgent = string.Format("Sonarr/{0} ({1} {2})",
BuildInfo.Version, BuildInfo.Version,
OsInfo.Os, OsInfo.Version.ToString(2)); OsInfo.Os, OsInfo.Version.ToString(2));
UserAgentSimplified = string.Format("Sonarr/{0}",
BuildInfo.Version.ToString(2));
} }
} }
} }

@ -30,6 +30,7 @@ namespace NzbDrone.Core.Annotations
Hidden, Hidden,
Tag, Tag,
Action, Action,
Url Url,
Captcha
} }
} }

@ -50,7 +50,7 @@ namespace NzbDrone.Core.Download
public ProviderDefinition Definition { get; set; } public ProviderDefinition Definition { get; set; }
public object ConnectData(string stage, IDictionary<string, object> query) { return null; } public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
protected TSettings Settings protected TSettings Settings
{ {

@ -0,0 +1,19 @@
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareCaptchaException : NzbDroneException
{
public HttpResponse Response { get; set; }
public CloudFlareCaptchaRequest CaptchaRequest { get; set; }
public CloudFlareCaptchaException(HttpResponse response, CloudFlareCaptchaRequest captchaRequest)
: base("Unable to access {0}, blocked by CloudFlare CAPTCHA. Likely due to shared-IP VPN.", response.Request.Url.Host)
{
Response = response;
CaptchaRequest = captchaRequest;
}
}
}

@ -0,0 +1,15 @@
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareCaptchaRequest
{
public string Host { get; set; }
public string SiteKey { get; set; }
public string Ray { get; set; }
public string SecretToken { get; set; }
public HttpUri ResponseUrl { get; set; }
}
}

@ -0,0 +1,56 @@
using System.Net;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.Http.CloudFlare
{
public class CloudFlareHttpInterceptor : IHttpRequestInterceptor
{
private readonly Logger _logger;
private const string _cloudFlareChallengeScript = "cdn-cgi/scripts/cf.challenge.js";
private static readonly Regex _cloudFlareRegex = new Regex(@"data-ray=""(?<Ray>[\w-_]+)"".*?data-sitekey=""(?<SiteKey>[\w-_]+)"".*?data-stoken=""(?<SecretToken>[\w-_]+)""", RegexOptions.Compiled);
public CloudFlareHttpInterceptor(Logger logger)
{
_logger = logger;
}
public HttpRequest PreRequest(HttpRequest request)
{
return request;
}
public HttpResponse PostResponse(HttpResponse response)
{
if (response.StatusCode == HttpStatusCode.Forbidden && response.Content.Contains(_cloudFlareChallengeScript))
{
_logger.Error("CloudFlare CAPTCHA block on {0}", response.Request.Url);
throw new CloudFlareCaptchaException(response, CreateCaptchaRequest(response));
}
return response;
}
private CloudFlareCaptchaRequest CreateCaptchaRequest(HttpResponse response)
{
var match = _cloudFlareRegex.Match(response.Content);
if (!match.Success)
{
return null;
}
return new CloudFlareCaptchaRequest
{
Host = response.Request.Url.Host,
SiteKey = match.Groups["SiteKey"].Value,
Ray = match.Groups["Ray"].Value,
SecretToken = match.Groups["SecretToken"].Value,
ResponseUrl = response.Request.Url + new HttpUri("/cdn-cgi/l/chk_captcha")
};
}
}
}

@ -8,6 +8,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@ -21,7 +22,7 @@ namespace NzbDrone.Core.Indexers
{ {
protected const int MaxNumResultsPerQuery = 1000; protected const int MaxNumResultsPerQuery = 1000;
private readonly IHttpClient _httpClient; protected readonly IHttpClient _httpClient;
public override bool SupportsRss { get { return true; } } public override bool SupportsRss { get { return true; } }
public override bool SupportsSearch { get { return true; } } public override bool SupportsSearch { get { return true; } }
@ -313,6 +314,10 @@ namespace NzbDrone.Core.Indexers
{ {
_logger.Warn("Request limit reached"); _logger.Warn("Request limit reached");
} }
catch (CloudFlareCaptchaException)
{
return new ValidationFailure("CaptchaToken", "Site protected by CloudFlare CAPTCHA. Valid CAPTCHA token required.");
}
catch (UnsupportedFeedException ex) catch (UnsupportedFeedException ex)
{ {
_logger.Warn(ex, "Indexer feed is not supported"); _logger.Warn(ex, "Indexer feed is not supported");

@ -65,7 +65,8 @@ namespace NzbDrone.Core.Indexers
} }
public virtual ProviderDefinition Definition { get; set; } public virtual ProviderDefinition Definition { get; set; }
public object ConnectData(string stage, IDictionary<string, object> query) { return null; }
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
protected TSettings Settings protected TSettings Settings
{ {

@ -1,8 +1,13 @@
using System; using System;
using System.Collections.Generic;
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Http.CloudFlare;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Rarbg namespace NzbDrone.Core.Indexers.Rarbg
{ {
@ -30,5 +35,78 @@ namespace NzbDrone.Core.Indexers.Rarbg
{ {
return new RarbgParser(); return new RarbgParser();
} }
public override object RequestAction(string action, IDictionary<string, string> query)
{
if (action == "checkCaptcha")
{
Settings.Validate().Filter("BaseUrl").ThrowOnError();
try
{
var request = new HttpRequestBuilder(Settings.BaseUrl.Trim('/'))
.Resource("/pubapi_v2.php?get_token=get_token")
.Accept(HttpAccept.Json)
.Build();
_httpClient.Get(request);
}
catch (CloudFlareCaptchaException ex)
{
return new
{
captchaRequest = new
{
host = ex.CaptchaRequest.Host,
ray = ex.CaptchaRequest.Ray,
siteKey = ex.CaptchaRequest.SiteKey,
secretToken = ex.CaptchaRequest.SecretToken,
responseUrl = ex.CaptchaRequest.ResponseUrl.FullUri,
}
};
}
return new
{
captchaToken = ""
};
}
else if (action == "getCaptchaCookie")
{
if (query["responseUrl"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam responseUrl invalid.");
}
if (query["ray"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam ray invalid.");
}
if (query["captchaResponse"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam captchaResponse invalid.");
}
var request = new HttpRequestBuilder(query["responseUrl"])
.AddQueryParam("id", query["ray"])
.AddQueryParam("g-recaptcha-response", query["captchaResponse"])
.Build();
request.UseSimplifiedUserAgent = true;
request.AllowAutoRedirect = false;
var response = _httpClient.Get(request);
var cfClearanceCookie = response.GetCookies()["cf_clearance"];
return new
{
captchaToken = cfClearanceCookie
};
}
return new { };
}
} }
} }

@ -79,6 +79,12 @@ namespace NzbDrone.Core.Indexers.Rarbg
.Resource("/pubapi_v2.php") .Resource("/pubapi_v2.php")
.Accept(HttpAccept.Json); .Accept(HttpAccept.Json);
if (Settings.CaptchaToken.IsNotNullOrWhiteSpace())
{
requestBuilder.UseSimplifiedUserAgent = true;
requestBuilder.SetCookie("cf_clearance", Settings.CaptchaToken);
}
requestBuilder.AddQueryParam("mode", mode); requestBuilder.AddQueryParam("mode", mode);
if (tvdbId.HasValue) if (tvdbId.HasValue)

@ -30,6 +30,9 @@ namespace NzbDrone.Core.Indexers.Rarbg
[FieldDefinition(1, Type = FieldType.Checkbox, Label = "Ranked Only", HelpText = "Only include ranked results.")] [FieldDefinition(1, Type = FieldType.Checkbox, Label = "Ranked Only", HelpText = "Only include ranked results.")]
public bool RankedOnly { get; set; } public bool RankedOnly { get; set; }
[FieldDefinition(2, Type = FieldType.Captcha, Label = "CAPTCHA Token", HelpText = "CAPTCHA Clearance token used to handle CloudFlare Anti-DDOS measures on shared-ip VPNs.")]
public string CaptchaToken { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));

@ -2,6 +2,7 @@ using System;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
namespace NzbDrone.Core.Indexers.Rarbg namespace NzbDrone.Core.Indexers.Rarbg
@ -28,9 +29,17 @@ namespace NzbDrone.Core.Indexers.Rarbg
{ {
return _tokenCache.Get(settings.BaseUrl, () => return _tokenCache.Get(settings.BaseUrl, () =>
{ {
var url = settings.BaseUrl.Trim('/') + "/pubapi_v2.php?get_token=get_token"; var requestBuilder = new HttpRequestBuilder(settings.BaseUrl.Trim('/'))
.Resource("/pubapi_v2.php?get_token=get_token")
.Accept(HttpAccept.Json);
var response = _httpClient.Get<JObject>(new HttpRequest(url, HttpAccept.Json)); if (settings.CaptchaToken.IsNotNullOrWhiteSpace())
{
requestBuilder.UseSimplifiedUserAgent = true;
requestBuilder.SetCookie("cf_clearance", settings.CaptchaToken);
}
var response = _httpClient.Get<JObject>(requestBuilder.Build());
return response.Resource["token"].ToString(); return response.Resource["token"].ToString();
}, TimeSpan.FromMinutes(14.0)); }, TimeSpan.FromMinutes(14.0));

@ -52,7 +52,7 @@ namespace NzbDrone.Core.Metadata
public abstract List<ImageFileResult> SeasonImages(Series series, Season season); public abstract List<ImageFileResult> SeasonImages(Series series, Season season);
public abstract List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile); public abstract List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
public object ConnectData(string stage, IDictionary<string, object> query) { return null; } public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
protected TSettings Settings protected TSettings Settings
{ {

@ -61,7 +61,7 @@ namespace NzbDrone.Core.Notifications
return GetType().Name; return GetType().Name;
} }
public virtual object ConnectData(string stage, IDictionary<string, object> query) { return null; } public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
} }
} }

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Notifications.Twitter
{
public class OAuthToken
{
public string AccessToken { get; set; }
public string AccessTokenSecret { get; set; }
}
}

@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation.Results; using FluentValidation.Results;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Notifications.Twitter namespace NzbDrone.Core.Notifications.Twitter
{ {
@ -34,26 +36,45 @@ namespace NzbDrone.Core.Notifications.Twitter
{ {
} }
public override object ConnectData(string stage, IDictionary<string, object> query) public override object RequestAction(string action, IDictionary<string, string> query)
{ {
if (stage == "step1") if (action == "startOAuth")
{ {
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
if (query["callbackUrl"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam callbackUrl invalid.");
}
var oauthRedirectUrl = _twitterService.GetOAuthRedirect(Settings.ConsumerKey, Settings.ConsumerSecret, query["callbackUrl"]);
return new return new
{ {
nextStep = "step2", oauthUrl = oauthRedirectUrl
action = "openWindow",
url = _twitterService.GetOAuthRedirect(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["callbackUrl"].ToString())
}; };
} }
else if (stage == "step2") else if (action == "getOAuthToken")
{ {
Settings.Validate().Filter("ConsumerKey", "ConsumerSecret").ThrowOnError();
if (query["oauth_token"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam oauth_token invalid.");
}
if (query["oauth_verifier"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam oauth_verifier invalid.");
}
var oauthToken = _twitterService.GetOAuthToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["oauth_token"], query["oauth_verifier"]);
return new return new
{ {
action = "updateFields", accessToken = oauthToken.AccessToken,
fields = _twitterService.GetOAuthToken(query["consumerKey"].ToString(), query["consumerSecret"].ToString(), query["oauth_token"].ToString(), query["oauth_verifier"].ToString()) accessTokenSecret = oauthToken.AccessTokenSecret
}; };
} }
return new {}; return new { };
} }
public override string Name public override string Name

@ -16,7 +16,7 @@ namespace NzbDrone.Core.Notifications.Twitter
void SendNotification(string message, TwitterSettings settings); void SendNotification(string message, TwitterSettings settings);
ValidationFailure Test(TwitterSettings settings); ValidationFailure Test(TwitterSettings settings);
string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl); string GetOAuthRedirect(string consumerKey, string consumerSecret, string callbackUrl);
object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier); OAuthToken GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier);
} }
public class TwitterService : ITwitterService public class TwitterService : ITwitterService
@ -43,14 +43,14 @@ namespace NzbDrone.Core.Notifications.Twitter
return HttpUtility.ParseQueryString(response.Content); return HttpUtility.ParseQueryString(response.Content);
} }
public object GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier) public OAuthToken GetOAuthToken(string consumerKey, string consumerSecret, string oauthToken, string oauthVerifier)
{ {
// Creating a new instance with a helper method // Creating a new instance with a helper method
var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier); var oAuthRequest = OAuthRequest.ForAccessToken(consumerKey, consumerSecret, oauthToken, "", oauthVerifier);
oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token"; oAuthRequest.RequestUrl = "https://api.twitter.com/oauth/access_token";
var qscoll = OAuthQuery(oAuthRequest); var qscoll = OAuthQuery(oAuthRequest);
return new return new OAuthToken
{ {
AccessToken = qscoll["oauth_token"], AccessToken = qscoll["oauth_token"],
AccessTokenSecret = qscoll["oauth_token_secret"] AccessTokenSecret = qscoll["oauth_token_secret"]

@ -1,4 +1,5 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -19,6 +20,10 @@ namespace NzbDrone.Core.Notifications.Twitter
RuleFor(c => c.DirectMessage).Equal(true) RuleFor(c => c.DirectMessage).Equal(true)
.WithMessage("Using Direct Messaging is recommended, or use a private account.") .WithMessage("Using Direct Messaging is recommended, or use a private account.")
.AsWarning(); .AsWarning();
RuleFor(c => c.AuthorizeNotification).Empty()
.When(c => c.AccessToken.IsNullOrWhiteSpace() || c.AccessTokenSecret.IsNullOrWhiteSpace())
.WithMessage("Authenticate app.");
} }
} }

@ -527,6 +527,9 @@
<Compile Include="Housekeeping\HousekeepingCommand.cs" /> <Compile Include="Housekeeping\HousekeepingCommand.cs" />
<Compile Include="Housekeeping\HousekeepingService.cs" /> <Compile Include="Housekeeping\HousekeepingService.cs" />
<Compile Include="Housekeeping\IHousekeepingTask.cs" /> <Compile Include="Housekeeping\IHousekeepingTask.cs" />
<Compile Include="Http\CloudFlare\CloudFlareCaptchaException.cs" />
<Compile Include="Http\CloudFlare\CloudFlareCaptchaRequest.cs" />
<Compile Include="Http\CloudFlare\CloudFlareHttpInterceptor.cs" />
<Compile Include="Http\HttpProxySettingsProvider.cs" /> <Compile Include="Http\HttpProxySettingsProvider.cs" />
<Compile Include="Http\TorcacheHttpInterceptor.cs" /> <Compile Include="Http\TorcacheHttpInterceptor.cs" />
<Compile Include="Indexers\BitMeTv\BitMeTv.cs" /> <Compile Include="Indexers\BitMeTv\BitMeTv.cs" />
@ -810,6 +813,7 @@
<Compile Include="Notifications\Synology\SynologyIndexer.cs" /> <Compile Include="Notifications\Synology\SynologyIndexer.cs" />
<Compile Include="Notifications\Synology\SynologyIndexerProxy.cs" /> <Compile Include="Notifications\Synology\SynologyIndexerProxy.cs" />
<Compile Include="Notifications\Synology\SynologyIndexerSettings.cs" /> <Compile Include="Notifications\Synology\SynologyIndexerSettings.cs" />
<Compile Include="Notifications\Twitter\OAuthToken.cs" />
<Compile Include="Notifications\Twitter\TwitterException.cs" /> <Compile Include="Notifications\Twitter\TwitterException.cs" />
<Compile Include="Notifications\Webhook\WebhookEpisode.cs" /> <Compile Include="Notifications\Webhook\WebhookEpisode.cs" />
<Compile Include="Notifications\Webhook\WebhookException.cs" /> <Compile Include="Notifications\Webhook\WebhookException.cs" />
@ -1054,6 +1058,7 @@
<Compile Include="Validation\FolderValidator.cs" /> <Compile Include="Validation\FolderValidator.cs" />
<Compile Include="Validation\IpValidation.cs" /> <Compile Include="Validation\IpValidation.cs" />
<Compile Include="Validation\LanguageValidator.cs" /> <Compile Include="Validation\LanguageValidator.cs" />
<Compile Include="Validation\NzbDroneValidationExtensions.cs" />
<Compile Include="Validation\NzbDroneValidationFailure.cs" /> <Compile Include="Validation\NzbDroneValidationFailure.cs" />
<Compile Include="Validation\NzbDroneValidationResult.cs" /> <Compile Include="Validation\NzbDroneValidationResult.cs" />
<Compile Include="Validation\NzbDroneValidationState.cs" /> <Compile Include="Validation\NzbDroneValidationState.cs" />

@ -12,6 +12,6 @@ namespace NzbDrone.Core.ThingiProvider
IEnumerable<ProviderDefinition> DefaultDefinitions { get; } IEnumerable<ProviderDefinition> DefaultDefinitions { get; }
ProviderDefinition Definition { get; set; } ProviderDefinition Definition { get; set; }
ValidationResult Test(); ValidationResult Test();
object ConnectData(string stage, IDictionary<string, object> query); object RequestAction(string stage, IDictionary<string, string> query);
} }
} }

@ -20,6 +20,6 @@ namespace NzbDrone.Core.ThingiProvider
void SetProviderCharacteristics(TProvider provider, TProviderDefinition definition); void SetProviderCharacteristics(TProvider provider, TProviderDefinition definition);
TProvider GetInstance(TProviderDefinition definition); TProvider GetInstance(TProviderDefinition definition);
ValidationResult Test(TProviderDefinition definition); ValidationResult Test(TProviderDefinition definition);
object ConnectData(TProviderDefinition definition, string stage, IDictionary<string, object> query ); object RequestAction(TProviderDefinition definition, string action, IDictionary<string, string> query);
} }
} }

@ -81,9 +81,9 @@ namespace NzbDrone.Core.ThingiProvider
return GetInstance(definition).Test(); return GetInstance(definition).Test();
} }
public object ConnectData(TProviderDefinition definition, string stage, IDictionary<string, object> query) public object RequestAction(TProviderDefinition definition, string action, IDictionary<string, string> query)
{ {
return GetInstance(definition).ConnectData(stage, query); return GetInstance(definition).RequestAction(action, query);
} }
public List<TProvider> GetAvailableProviders() public List<TProvider> GetAvailableProviders()

@ -0,0 +1,24 @@
using System;
using System.Linq;
using FluentValidation;
namespace NzbDrone.Core.Validation
{
public static class NzbDroneValidationExtensions
{
public static NzbDroneValidationResult Filter(this NzbDroneValidationResult result, params string[] fields)
{
var failures = result.Failures.Where(v => fields.Contains(v.PropertyName)).ToArray();
return new NzbDroneValidationResult(failures);
}
public static void ThrowOnError(this NzbDroneValidationResult result)
{
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
}
}

@ -2,6 +2,6 @@
<label class="col-sm-3 control-label"></label> <label class="col-sm-3 control-label"></label>
<div class="col-sm-5"> <div class="col-sm-5">
<button class="form-control {{name}}" data-value="{{value}}">{{label}}</button> <button class="form-control {{name}}" validation-name="{{name}}" data-value="{{value}}">{{label}}</button>
</div> </div>
</div> </div>

@ -0,0 +1,14 @@
<div class="form-group {{#if advanced}}advanced-setting{{/if}}">
<label class="col-sm-3 control-label">{{label}}</label>
<div class="col-sm-5">
<div class="input-group">
<input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control x-captcha" readonly />
<span class="input-group-btn"><button class="btn btn-primary x-captcha-refresh" title="Refresh CAPTCHA Token"><i class="icon-sonarr-refresh" /></button></span>
</div>
</div>
<span class="col-sm-1 help-inline">
<i class="icon-sonarr-form-warning" title="Expires periodically and will need to be refreshed. Refreshing the CAPTCHA Token will embed a temporary Google reCaptcha widget on this page."/>
</span>
</div>

@ -49,6 +49,10 @@ var _fieldBuilder = function(field) {
return _templateRenderer.call(field, 'Form/ActionTemplate'); return _templateRenderer.call(field, 'Form/ActionTemplate');
} }
if (field.type === 'captcha') {
return _templateRenderer.call(field, 'Form/CaptchaTemplate');
}
return _templateRenderer.call(field, 'Form/TextboxTemplate'); return _templateRenderer.call(field, 'Form/TextboxTemplate');
}; };

@ -1,3 +1,5 @@
var _ = require('underscore');
var $ = require('jquery');
var vent = require('vent'); var vent = require('vent');
var Marionette = require('marionette'); var Marionette = require('marionette');
var DeleteView = require('../Delete/IndexerDeleteView'); var DeleteView = require('../Delete/IndexerDeleteView');
@ -12,7 +14,8 @@ var view = Marionette.ItemView.extend({
template : 'Settings/Indexers/Edit/IndexerEditViewTemplate', template : 'Settings/Indexers/Edit/IndexerEditViewTemplate',
events : { events : {
'click .x-back' : '_back' 'click .x-back' : '_back',
'click .x-captcha-refresh' : '_onRefreshCaptcha'
}, },
_deleteView : DeleteView, _deleteView : DeleteView,
@ -38,6 +41,77 @@ var view = Marionette.ItemView.extend({
} }
require('../Add/IndexerSchemaModal').open(this.targetCollection); require('../Add/IndexerSchemaModal').open(this.targetCollection);
},
_onRefreshCaptcha : function(event) {
var self = this;
var target = $(event.target).parents('.input-group');
this.ui.indicator.show();
this.model.requestAction("checkCaptcha")
.then(function(result) {
if (!result.captchaRequest) {
self.model.setFieldValue('CaptchaToken', '');
return result;
}
return self._showCaptcha(target, result.captchaRequest);
})
.always(function() {
self.ui.indicator.hide();
});
},
_showCaptcha : function(target, captchaRequest) {
var self = this;
var widget = $('<div class="g-recaptcha"></div>').insertAfter(target);
return this._loadRecaptchaWidget(widget[0], captchaRequest.siteKey, captchaRequest.secretToken)
.then(function(captchaResponse) {
target.parents('.form-group').removeAllErrors();
widget.remove();
var queryParams = {
responseUrl : captchaRequest.responseUrl,
ray : captchaRequest.ray,
captchaResponse: captchaResponse
};
return self.model.requestAction("getCaptchaCookie", queryParams);
})
.then(function(response) {
self.model.setFieldValue('CaptchaToken', response.captchaToken);
});
},
_loadRecaptchaWidget : function(widget, sitekey, stoken) {
var promise = $.Deferred();
var renderWidget = function() {
window.grecaptcha.render(widget, {
'sitekey' : sitekey,
'stoken' : stoken,
'callback' : promise.resolve
});
};
if (window.grecaptcha) {
renderWidget();
} else {
window.grecaptchaLoadCallback = function() {
delete window.grecaptchaLoadCallback;
renderWidget();
};
$.getScript('https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadCallback&render=explicit')
.fail(function() { promise.reject(); });
}
return promise;
} }
}); });

@ -1,4 +1,5 @@
var _ = require('underscore'); var _ = require('underscore');
var $ = require('jquery');
var vent = require('vent'); var vent = require('vent');
var Marionette = require('marionette'); var Marionette = require('marionette');
var DeleteView = require('../Delete/NotificationDeleteView'); var DeleteView = require('../Delete/NotificationDeleteView');
@ -90,21 +91,45 @@ var view = Marionette.ItemView.extend({
this.ui.indicator.show(); this.ui.indicator.show();
var self = this; var self = this;
var callbackUrl = window.location.origin + '/oauth.html';
var fields = this.model.get('fields');
var consumerKeyObj = _.findWhere(fields, { name: 'ConsumerKey' });
var consumerSecretObj = _.findWhere(fields, { name: 'ConsumerSecret' });
var queryParams = {
callbackUrl: callbackUrl,
consumerKey: (consumerKeyObj ? consumerKeyObj.value : ''),
consumerSecret: (consumerSecretObj ? consumerSecretObj.value : '')
};
var promise = this.model.connectData(this.ui.authorizedNotificationButton.data('value'), queryParams); var promise = this.model.requestAction('startOAuth', { callbackUrl: window.location.origin + '/oauth.html' })
.then(function(response) {
return self._showOAuthWindow(response.oauthUrl);
})
.then(function(responseQueryParams) {
return self.model.requestAction('getOAuthToken', responseQueryParams);
})
.then(function(response) {
self.model.setFieldValue('AccessToken', response.accessToken);
self.model.setFieldValue('AccessTokenSecret', response.accessTokenSecret);
});
promise.always(function() { promise.always(function() {
self.ui.indicator.hide(); self.ui.indicator.hide();
}); });
},
_showOAuthWindow : function(oauthUrl) {
var promise = $.Deferred();
window.open(oauthUrl);
var selfWindow = window;
selfWindow.onCompleteOauth = function(query, callback) {
delete selfWindow.onCompleteOauth;
var queryParams = {};
var splitQuery = query.substring(1).split('&');
_.each(splitQuery, function (param) {
var paramSplit = param.split('=');
queryParams[paramSplit[0]] = paramSplit[1];
});
callback();
promise.resolve(queryParams);
};
return promise;
} }
}); });

@ -4,93 +4,38 @@ var DeepModel = require('backbone.deepmodel');
var Messenger = require('../Shared/Messenger'); var Messenger = require('../Shared/Messenger');
module.exports = DeepModel.extend({ module.exports = DeepModel.extend({
connectData : function(action, initialQueryParams) {
getFieldValue : function(name) {
var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name);
return this.get('fields.' + index + '.value');
},
setFieldValue : function(name, value) {
var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name);
return this.set('fields.' + index + '.value', value);
},
requestAction : function(action, queryParams) {
var self = this; var self = this;
this.trigger('connect:sync'); this.trigger('validation:sync');
var promise = $.Deferred(); var params = {
url : this.collection.url + '/action/' + action,
var callAction = function(action, queryParams) { contentType : 'application/json',
data : JSON.stringify(this.toJSON()),
if (queryParams) { type : 'POST',
action = action + '?' + $.param(queryParams, true); isValidatedCall : true
}
var params = {
url : self.collection.url + '/connectData/' + action,
contentType : 'application/json',
data : JSON.stringify(self.toJSON()),
type : 'POST',
isValidatedCall : true
};
var ajaxPromise = $.ajax(params);
ajaxPromise.fail(promise.reject);
ajaxPromise.success(function(response) {
if (response.action)
{
if (response.action === 'openWindow')
{
window.open(response.url);
var selfWindow = window;
selfWindow.onCompleteOauth = function(query, callback) {
delete selfWindow.onCompleteOauth;
if (response.nextStep) {
var queryParams = {};
var splitQuery = query.substring(1).split('&');
_.each(splitQuery, function (param) {
var paramSplit = param.split('=');
queryParams[paramSplit[0]] = paramSplit[1];
});
callAction(response.nextStep, _.extend(initialQueryParams, queryParams));
}
else {
promise.resolve(response);
}
callback();
};
return;
}
else if (response.action === 'updateFields')
{
_.each(self.get('fields'), function (value, index) {
var fieldValue = _.find(response.fields, function (field, key) {
return key === value.name;
});
if (fieldValue) {
self.set('fields.' + index + '.value', fieldValue);
}
});
}
}
if (response.nextStep) {
callAction(response.nextStep, initialQueryParams);
}
else {
promise.resolve(response);
}
});
}; };
callAction(action, initialQueryParams); if (queryParams) {
params.url += '?' + $.param(queryParams, true);
}
Messenger.monitor({ var promise = $.ajax(params);
promise : promise,
successMessage : 'Connecting for \'{0}\' succeeded'.format(this.get('name')),
errorMessage : 'Connecting for \'{0}\' failed'.format(this.get('name'))
});
promise.fail(function(response) { promise.fail(function(response) {
self.trigger('connect:failed', response); self.trigger('validation:failed', response);
}); });
return promise; return promise;

@ -75,6 +75,8 @@ module.exports = function() {
}; };
$.fn.removeAllErrors = function() { $.fn.removeAllErrors = function() {
this.removeClass('has-error');
this.removeClass('has-warning');
this.find('.has-error').removeClass('has-error'); this.find('.has-error').removeClass('has-error');
this.find('.has-warning').removeClass('has-warning'); this.find('.has-warning').removeClass('has-warning');
this.find('.error').removeClass('error'); this.find('.error').removeClass('error');

Loading…
Cancel
Save