mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Support for indexers with image CAPTCHAs
This commit is contained in:
84
frontend/src/Components/Form/CardigannCaptchaInput.js
Normal file
84
frontend/src/Components/Form/CardigannCaptchaInput.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import FormInputButton from './FormInputButton';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './CaptchaInput.css';
|
||||||
|
|
||||||
|
function CardigannCaptchaInput(props) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
|
type,
|
||||||
|
refreshing,
|
||||||
|
contentType,
|
||||||
|
imageData,
|
||||||
|
onChange,
|
||||||
|
onRefreshPress
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const img = `data:${contentType};base64,${imageData}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.captchaInputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles.hasButton,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
)}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormInputButton
|
||||||
|
onPress={onRefreshPress}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.REFRESH}
|
||||||
|
isSpinning={refreshing}
|
||||||
|
/>
|
||||||
|
</FormInputButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
type === 'image' &&
|
||||||
|
<div className={styles.recaptchaWrapper}>
|
||||||
|
<img
|
||||||
|
src={img}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CardigannCaptchaInput.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
|
hasWarning: PropTypes.bool,
|
||||||
|
type: PropTypes.string,
|
||||||
|
refreshing: PropTypes.bool.isRequired,
|
||||||
|
contentType: PropTypes.string.isRequired,
|
||||||
|
imageData: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
|
onCaptchaChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
CardigannCaptchaInput.defaultProps = {
|
||||||
|
className: styles.input,
|
||||||
|
value: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardigannCaptchaInput;
|
@@ -0,0 +1,77 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { getCaptchaCookie, refreshCaptcha, resetCaptcha } from 'Store/Actions/captchaActions';
|
||||||
|
import CardigannCaptchaInput from './CardigannCaptchaInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.captcha,
|
||||||
|
(captcha) => {
|
||||||
|
return captcha;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
refreshCaptcha,
|
||||||
|
getCaptchaCookie,
|
||||||
|
resetCaptcha
|
||||||
|
};
|
||||||
|
|
||||||
|
class CardigannCaptchaInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.onRefreshPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
this.props.resetCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRefreshPress = () => {
|
||||||
|
const {
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
name,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onChange({ name, value: '' });
|
||||||
|
this.props.resetCaptcha();
|
||||||
|
this.props.refreshCaptcha({ provider, providerData });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CardigannCaptchaInput
|
||||||
|
{...this.props}
|
||||||
|
onRefreshPress={this.onRefreshPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CardigannCaptchaInputConnector.propTypes = {
|
||||||
|
provider: PropTypes.string.isRequired,
|
||||||
|
providerData: PropTypes.object.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
token: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
refreshCaptcha: PropTypes.func.isRequired,
|
||||||
|
getCaptchaCookie: PropTypes.func.isRequired,
|
||||||
|
resetCaptcha: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CardigannCaptchaInputConnector);
|
@@ -6,6 +6,7 @@ import translate from 'Utilities/String/translate';
|
|||||||
import AutoCompleteInput from './AutoCompleteInput';
|
import AutoCompleteInput from './AutoCompleteInput';
|
||||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
||||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||||
|
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
|
||||||
import CheckInput from './CheckInput';
|
import CheckInput from './CheckInput';
|
||||||
import DeviceInputConnector from './DeviceInputConnector';
|
import DeviceInputConnector from './DeviceInputConnector';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
@@ -37,6 +38,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.CAPTCHA:
|
case inputTypes.CAPTCHA:
|
||||||
return CaptchaInputConnector;
|
return CaptchaInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.CARDIGANNCAPTCHA:
|
||||||
|
return CardigannCaptchaInputConnector;
|
||||||
|
|
||||||
case inputTypes.CHECK:
|
case inputTypes.CHECK:
|
||||||
return CheckInput;
|
return CheckInput;
|
||||||
|
|
||||||
|
@@ -10,6 +10,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'captcha':
|
case 'captcha':
|
||||||
return inputTypes.CAPTCHA;
|
return inputTypes.CAPTCHA;
|
||||||
|
case 'cardigannCaptcha':
|
||||||
|
return inputTypes.CARDIGANNCAPTCHA;
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return inputTypes.CHECK;
|
return inputTypes.CHECK;
|
||||||
case 'device':
|
case 'device':
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
export const AUTO_COMPLETE = 'autoComplete';
|
export const AUTO_COMPLETE = 'autoComplete';
|
||||||
export const AVAILABILITY_SELECT = 'availabilitySelect';
|
export const AVAILABILITY_SELECT = 'availabilitySelect';
|
||||||
export const CAPTCHA = 'captcha';
|
export const CAPTCHA = 'captcha';
|
||||||
|
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
|
||||||
export const CHECK = 'check';
|
export const CHECK = 'check';
|
||||||
export const DEVICE = 'device';
|
export const DEVICE = 'device';
|
||||||
export const INFO = 'info';
|
export const INFO = 'info';
|
||||||
@@ -21,6 +22,7 @@ export const all = [
|
|||||||
AUTO_COMPLETE,
|
AUTO_COMPLETE,
|
||||||
AVAILABILITY_SELECT,
|
AVAILABILITY_SELECT,
|
||||||
CAPTCHA,
|
CAPTCHA,
|
||||||
|
CARDIGANNCAPTCHA,
|
||||||
CHECK,
|
CHECK,
|
||||||
DEVICE,
|
DEVICE,
|
||||||
INFO,
|
INFO,
|
||||||
|
@@ -20,7 +20,10 @@ export const defaultState = {
|
|||||||
secretToken: null,
|
secretToken: null,
|
||||||
ray: null,
|
ray: null,
|
||||||
stoken: null,
|
stoken: null,
|
||||||
responseUrl: null
|
responseUrl: null,
|
||||||
|
type: null,
|
||||||
|
contentType: null,
|
||||||
|
imageData: null
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
|
||||||
|
{
|
||||||
|
public class Captcha
|
||||||
|
{
|
||||||
|
public string Type { get; set; } = "image";
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
public byte[] ImageData { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,10 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.IndexerVersions;
|
using NzbDrone.Core.IndexerVersions;
|
||||||
@@ -13,6 +16,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
public class Cardigann : HttpIndexerBase<CardigannSettings>
|
public class Cardigann : HttpIndexerBase<CardigannSettings>
|
||||||
{
|
{
|
||||||
private readonly IIndexerDefinitionUpdateService _definitionService;
|
private readonly IIndexerDefinitionUpdateService _definitionService;
|
||||||
|
private readonly ICached<CardigannRequestGenerator> _generatorCache;
|
||||||
|
|
||||||
public override string Name => "Cardigann";
|
public override string Name => "Cardigann";
|
||||||
public override string BaseUrl => "";
|
public override string BaseUrl => "";
|
||||||
@@ -23,21 +27,24 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
|
|
||||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||||
{
|
{
|
||||||
return new CardigannRequestGenerator(_configService,
|
return _generatorCache.Get(Settings.DefinitionFile, () =>
|
||||||
_definitionService.GetDefinition(Settings.DefinitionFile),
|
new CardigannRequestGenerator(_configService,
|
||||||
Settings,
|
_definitionService.GetDefinition(Settings.DefinitionFile),
|
||||||
_logger)
|
_logger)
|
||||||
{
|
{
|
||||||
HttpClient = _httpClient
|
HttpClient = _httpClient,
|
||||||
};
|
Settings = Settings
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IParseIndexerResponse GetParser()
|
public override IParseIndexerResponse GetParser()
|
||||||
{
|
{
|
||||||
return new CardigannParser(_configService,
|
return new CardigannParser(_configService,
|
||||||
_definitionService.GetDefinition(Settings.DefinitionFile),
|
_definitionService.GetDefinition(Settings.DefinitionFile),
|
||||||
Settings,
|
_logger)
|
||||||
_logger);
|
{
|
||||||
|
Settings = Settings
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
public override IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||||
@@ -55,10 +62,12 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
IHttpClient httpClient,
|
IHttpClient httpClient,
|
||||||
IIndexerStatusService indexerStatusService,
|
IIndexerStatusService indexerStatusService,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
|
ICacheManager cacheManager,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(httpClient, indexerStatusService, configService, logger)
|
: base(httpClient, indexerStatusService, configService, logger)
|
||||||
{
|
{
|
||||||
_definitionService = definitionService;
|
_definitionService = definitionService;
|
||||||
|
_generatorCache = cacheManager.GetRollingCache<CardigannRequestGenerator>(GetType(), "CardigannGeneratorCache", TimeSpan.FromMinutes(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndexerDefinition GetDefinition(CardigannMetaDefinition definition)
|
private IndexerDefinition GetDefinition(CardigannMetaDefinition definition)
|
||||||
@@ -71,6 +80,16 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
|
|
||||||
var settings = definition.Settings ?? defaultSettings;
|
var settings = definition.Settings ?? defaultSettings;
|
||||||
|
|
||||||
|
if (definition.Login?.Captcha != null)
|
||||||
|
{
|
||||||
|
settings.Add(new SettingsField
|
||||||
|
{
|
||||||
|
Name = "cardigannCaptcha",
|
||||||
|
Type = "cardigannCaptcha",
|
||||||
|
Label = "CAPTCHA"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new IndexerDefinition
|
return new IndexerDefinition
|
||||||
{
|
{
|
||||||
Enable = true,
|
Enable = true,
|
||||||
@@ -93,6 +112,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
|
|
||||||
SetCookieFunctions(generator);
|
SetCookieFunctions(generator);
|
||||||
|
|
||||||
|
generator.Settings = Settings;
|
||||||
|
|
||||||
return generator.CheckIfLoginIsNeeded(httpResponse);
|
return generator.CheckIfLoginIsNeeded(httpResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +123,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
|
|
||||||
SetCookieFunctions(generator);
|
SetCookieFunctions(generator);
|
||||||
|
|
||||||
|
generator.Settings = Settings;
|
||||||
|
|
||||||
await generator.DoLogin();
|
await generator.DoLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,5 +136,21 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (action == "checkCaptcha")
|
||||||
|
{
|
||||||
|
var generator = (CardigannRequestGenerator)GetRequestGenerator();
|
||||||
|
|
||||||
|
var result = generator.GetConfigurationForSetup(false).GetAwaiter().GetResult();
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
captchaRequest = result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
public class CardigannBase
|
public class CardigannBase
|
||||||
{
|
{
|
||||||
protected readonly CardigannDefinition _definition;
|
protected readonly CardigannDefinition _definition;
|
||||||
protected readonly CardigannSettings _settings;
|
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
protected readonly Encoding _encoding;
|
protected readonly Encoding _encoding;
|
||||||
protected readonly IConfigService _configService;
|
protected readonly IConfigService _configService;
|
||||||
@@ -48,14 +47,14 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
protected static readonly Regex _LogicFunctionRegex = new Regex(
|
protected static readonly Regex _LogicFunctionRegex = new Regex(
|
||||||
$@"\b({string.Join("|", _SupportedLogicFunctions.Select(Regex.Escape))})(?:\s+(\(?\.[^\)\s]+\)?|""[^""]+"")){{2,}}");
|
$@"\b({string.Join("|", _SupportedLogicFunctions.Select(Regex.Escape))})(?:\s+(\(?\.[^\)\s]+\)?|""[^""]+"")){{2,}}");
|
||||||
|
|
||||||
|
public CardigannSettings Settings { get; set; }
|
||||||
|
|
||||||
public CardigannBase(IConfigService configService,
|
public CardigannBase(IConfigService configService,
|
||||||
CardigannDefinition definition,
|
CardigannDefinition definition,
|
||||||
CardigannSettings settings,
|
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_definition = definition;
|
_definition = definition;
|
||||||
_settings = settings;
|
|
||||||
_encoding = Encoding.GetEncoding(definition.Encoding);
|
_encoding = Encoding.GetEncoding(definition.Encoding);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
@@ -224,7 +223,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
foreach (var setting in _definition.Settings)
|
foreach (var setting in _definition.Settings)
|
||||||
{
|
{
|
||||||
var name = ".Config." + setting.Name;
|
var name = ".Config." + setting.Name;
|
||||||
var value = _settings.ExtraFieldData.GetValueOrDefault(setting.Name, setting.Default);
|
var value = Settings.ExtraFieldData.GetValueOrDefault(setting.Name, setting.Default);
|
||||||
|
|
||||||
if (setting.Type != "password" && indexerLogging)
|
if (setting.Type != "password" && indexerLogging)
|
||||||
{
|
{
|
||||||
@@ -260,6 +259,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
{
|
{
|
||||||
variables[name] = value;
|
variables[name] = value;
|
||||||
}
|
}
|
||||||
|
else if (setting.Type == "cardigannCaptcha")
|
||||||
|
{
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
@@ -15,5 +15,6 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
public List<string> Legacylinks { get; set; }
|
public List<string> Legacylinks { get; set; }
|
||||||
public List<SettingsField> Settings { get; set; }
|
public List<SettingsField> Settings { get; set; }
|
||||||
public string Sha { get; set; }
|
public string Sha { get; set; }
|
||||||
|
public LoginBlock Login { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,9 +20,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
|
|
||||||
public CardigannParser(IConfigService configService,
|
public CardigannParser(IConfigService configService,
|
||||||
CardigannDefinition definition,
|
CardigannDefinition definition,
|
||||||
CardigannSettings settings,
|
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(configService, definition, settings, logger)
|
: base(configService, definition, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,9 +25,8 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
|
|
||||||
public CardigannRequestGenerator(IConfigService configService,
|
public CardigannRequestGenerator(IConfigService configService,
|
||||||
CardigannDefinition definition,
|
CardigannDefinition definition,
|
||||||
CardigannSettings settings,
|
|
||||||
Logger logger)
|
Logger logger)
|
||||||
: base(configService, definition, settings, logger)
|
: base(configService, definition, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +180,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
LogResponseContent = true,
|
LogResponseContent = true,
|
||||||
Method = HttpMethod.POST,
|
Method = HttpMethod.POST,
|
||||||
AllowAutoRedirect = true,
|
AllowAutoRedirect = true,
|
||||||
SuppressHttpError = true
|
SuppressHttpError = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var pair in pairs)
|
foreach (var pair in pairs)
|
||||||
@@ -339,46 +338,22 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
if (login.Captcha != null)
|
if (login.Captcha != null)
|
||||||
{
|
{
|
||||||
var captcha = login.Captcha;
|
var captcha = login.Captcha;
|
||||||
if (captcha.Type == "image")
|
Settings.ExtraFieldData.TryGetValue("CAPTCHA", out var captchaText);
|
||||||
|
if (captchaText != null)
|
||||||
{
|
{
|
||||||
_settings.ExtraFieldData.TryGetValue("CaptchaText", out var captchaText);
|
var input = captcha.Input;
|
||||||
if (captchaText != null)
|
if (login.Selectors)
|
||||||
{
|
{
|
||||||
var input = captcha.Input;
|
var inputElement = landingResultDocument.QuerySelector(captcha.Input);
|
||||||
if (login.Selectors)
|
if (inputElement == null)
|
||||||
{
|
{
|
||||||
var inputElement = landingResultDocument.QuerySelector(captcha.Input);
|
throw new CardigannConfigException(_definition, string.Format("Login failed: No captcha input found using {0}", 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;
|
input = inputElement.GetAttribute("name");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (captcha.Type == "text")
|
pairs[input] = (string)captchaText;
|
||||||
{
|
|
||||||
_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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +437,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
else if (login.Method == "cookie")
|
else if (login.Method == "cookie")
|
||||||
{
|
{
|
||||||
CookiesUpdater(null, null);
|
CookiesUpdater(null, null);
|
||||||
_settings.ExtraFieldData.TryGetValue("cookie", out var cookies);
|
Settings.ExtraFieldData.TryGetValue("cookie", out var cookies);
|
||||||
CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now + TimeSpan.FromDays(30));
|
CookiesUpdater(CookieUtil.CookieHeaderToDictionary((string)cookies), DateTime.Now + TimeSpan.FromDays(30));
|
||||||
}
|
}
|
||||||
else if (login.Method == "get")
|
else if (login.Method == "get")
|
||||||
@@ -557,13 +532,13 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task GetConfigurationForSetup(bool automaticlogin)
|
public async Task<Captcha> GetConfigurationForSetup(bool automaticlogin)
|
||||||
{
|
{
|
||||||
var login = _definition.Login;
|
var login = _definition.Login;
|
||||||
|
|
||||||
if (login == null || login.Method != "form")
|
if (login == null || login.Method != "form")
|
||||||
{
|
{
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginUrl = ResolvePath(login.Path);
|
var loginUrl = ResolvePath(login.Path);
|
||||||
@@ -588,7 +563,9 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
requestBuilder.SetCookies(Cookies);
|
requestBuilder.SetCookies(Cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
landingResult = await HttpClient.ExecuteAsync(requestBuilder.Build());
|
var request = requestBuilder.Build();
|
||||||
|
|
||||||
|
landingResult = await HttpClient.ExecuteAsync(request);
|
||||||
|
|
||||||
Cookies = landingResult.GetCookies();
|
Cookies = landingResult.GetCookies();
|
||||||
|
|
||||||
@@ -597,122 +574,60 @@ namespace NzbDrone.Core.Indexers.Cardigann
|
|||||||
//{
|
//{
|
||||||
// await FollowIfRedirect(landingResult, loginUrl.AbsoluteUri, overrideCookies: landingResult.Cookies, accumulateCookies: true);
|
// await FollowIfRedirect(landingResult, loginUrl.AbsoluteUri, overrideCookies: landingResult.Cookies, accumulateCookies: true);
|
||||||
//}
|
//}
|
||||||
var hasCaptcha = false;
|
|
||||||
var htmlParser = new HtmlParser();
|
var htmlParser = new HtmlParser();
|
||||||
landingResultDocument = htmlParser.ParseDocument(landingResult.Content);
|
landingResultDocument = htmlParser.ParseDocument(landingResult.Content);
|
||||||
|
|
||||||
|
Captcha captcha = null;
|
||||||
|
|
||||||
if (login.Captcha != null)
|
if (login.Captcha != null)
|
||||||
{
|
{
|
||||||
var captcha = login.Captcha;
|
captcha = await GetCaptcha(login);
|
||||||
if (captcha.Type == "image")
|
}
|
||||||
|
|
||||||
|
if (captcha != null && automaticlogin)
|
||||||
|
{
|
||||||
|
_logger.Error(string.Format("CardigannIndexer ({0}): Found captcha during automatic login, aborting", _definition.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return captcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Captcha> GetCaptcha(LoginBlock login)
|
||||||
|
{
|
||||||
|
var captcha = login.Captcha;
|
||||||
|
|
||||||
|
if (captcha.Type == "image")
|
||||||
|
{
|
||||||
|
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
|
||||||
|
if (captchaElement != null)
|
||||||
{
|
{
|
||||||
var captchaElement = landingResultDocument.QuerySelector(captcha.Selector);
|
var loginUrl = ResolvePath(login.Path);
|
||||||
if (captchaElement != null)
|
var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl);
|
||||||
{
|
|
||||||
hasCaptcha = true;
|
|
||||||
|
|
||||||
//TODO Bubble this to UI when we get a captcha so that user can action it
|
var request = new HttpRequestBuilder(captchaUrl.ToString())
|
||||||
//Jackett does this by inserting image or question into the extrasettings which then show up in the add modal
|
.SetCookies(landingResult.GetCookies())
|
||||||
//
|
.SetHeader("Referrer", loginUrl.AbsoluteUri)
|
||||||
//var captchaUrl = ResolvePath(captchaElement.GetAttribute("src"), loginUrl);
|
.Build();
|
||||||
//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 response = await HttpClient.ExecuteAsync(request);
|
||||||
//var captchaAnswer = new StringItem { Name = "Captcha Answer" };
|
|
||||||
|
|
||||||
//configData.AddDynamic("CaptchaChallenge", captchaChallenge);
|
return new Captcha
|
||||||
//configData.AddDynamic("CaptchaAnswer", captchaAnswer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_logger.Debug(string.Format("CardigannIndexer ({0}): No captcha image found", _definition.Id));
|
ContentType = response.Headers.ContentType,
|
||||||
}
|
ImageData = response.ResponseData
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new NotImplementedException(string.Format("Captcha type \"{0}\" is not implemented", captcha.Type));
|
_logger.Debug(string.Format("CardigannIndexer ({0}): No captcha image found", _definition.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (hasCaptcha && automaticlogin)
|
|
||||||
{
|
{
|
||||||
_logger.Error(string.Format("CardigannIndexer ({0}): Found captcha during automatic login, aborting", _definition.Id));
|
throw new NotImplementedException(string.Format("Captcha type \"{0}\" is not implemented", captcha.Type));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task<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 = await HttpClient.ExecuteAsync(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)
|
protected string GetRedirectDomainHint(string requestUrl, string redirectUrl)
|
||||||
|
@@ -78,6 +78,17 @@ namespace NzbDrone.Core.Indexers
|
|||||||
var settings = (CardigannSettings)definition.Settings;
|
var settings = (CardigannSettings)definition.Settings;
|
||||||
var defFile = _definitionService.GetDefinition(settings.DefinitionFile);
|
var defFile = _definitionService.GetDefinition(settings.DefinitionFile);
|
||||||
definition.ExtraFields = defFile.Settings;
|
definition.ExtraFields = defFile.Settings;
|
||||||
|
|
||||||
|
if (defFile.Login?.Captcha != null && !definition.ExtraFields.Any(x => x.Type == "cardigannCaptcha"))
|
||||||
|
{
|
||||||
|
definition.ExtraFields.Add(new SettingsField
|
||||||
|
{
|
||||||
|
Name = "cardigannCaptcha",
|
||||||
|
Type = "cardigannCaptcha",
|
||||||
|
Label = "CAPTCHA"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
definition.BaseUrl = defFile.Links.First();
|
definition.BaseUrl = defFile.Links.First();
|
||||||
definition.Privacy = defFile.Type == "private" ? IndexerPrivacy.Private : IndexerPrivacy.Public;
|
definition.Privacy = defFile.Type == "private" ? IndexerPrivacy.Private : IndexerPrivacy.Public;
|
||||||
definition.Capabilities = new IndexerCapabilities();
|
definition.Capabilities = new IndexerCapabilities();
|
||||||
|
@@ -97,8 +97,15 @@ namespace Prowlarr.Api.V1.Indexers
|
|||||||
{
|
{
|
||||||
if (!standardFields.Contains(field.Name))
|
if (!standardFields.Contains(field.Name))
|
||||||
{
|
{
|
||||||
var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name);
|
if (field.Name == "cardigannCaptcha")
|
||||||
settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value);
|
{
|
||||||
|
settings.ExtraFieldData["CAPTCHA"] = field.Value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var cardigannSetting = cardigannDefinition.Settings.FirstOrDefault(x => x.Name == field.Name);
|
||||||
|
settings.ExtraFieldData[field.Name] = MapValue(cardigannSetting, field.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +139,7 @@ namespace Prowlarr.Api.V1.Indexers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return value.ToString();
|
return value?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -174,7 +174,7 @@ namespace Prowlarr.Api.V1
|
|||||||
|
|
||||||
var data = _providerFactory.RequestAction(providerDefinition, name, query);
|
var data = _providerFactory.RequestAction(providerDefinition, name, query);
|
||||||
|
|
||||||
return Content(data.ToJson(), "application/json");
|
return Json(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)
|
protected virtual void Validate(TProviderDefinition definition, bool includeWarnings)
|
||||||
|
Reference in New Issue
Block a user