diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 3d1863784..f239a8bc8 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -232,7 +232,8 @@ private ValidationFailure TestConnection() catch (DownloadClientAuthenticationException ex) { _logger.Error(ex, ex.Message); - return new NzbDroneValidationFailure("Username", "Authentication failure") + + return new NzbDroneValidationFailure(Settings.ApiKey.IsNotNullOrWhiteSpace() ? "ApiKey" : "Username", "Authentication failure") { DetailedDescription = "Please verify your username and password." }; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index c15a2447f..91585440d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -337,13 +337,19 @@ public void SetForceStart(string hash, bool enabled, QBittorrentSettings setting ProcessRequest(request, settings); } - private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) + private static HttpRequestBuilder BuildRequest(QBittorrentSettings settings) { var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) { LogResponseContent = true, StoreRequestCookie = false }; + + if (settings.ApiKey.IsNotNullOrWhiteSpace()) + { + requestBuilder.Headers["Authorization"] = $"Bearer {settings.ApiKey}"; + } + return requestBuilder; } @@ -357,16 +363,39 @@ private TResult ProcessRequest(HttpRequestBuilder requestBuilder, QBitt private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings) { - AuthenticateClient(requestBuilder, settings); - var request = requestBuilder.Build(); request.LogResponseContent = true; + + if (settings.ApiKey.IsNotNullOrWhiteSpace()) + { + try + { + return _httpClient.Execute(request).Content; + } + catch (HttpException ex) + { + if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + _logger.Debug(ex, "qbitTorrent authentication failed."); + + throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex); + } + + throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); + } + catch (Exception ex) + { + throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex); + } + } + + AuthenticateClient(requestBuilder, settings); + request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden }; - HttpResponse response; try { - response = _httpClient.Execute(request); + var response = _httpClient.Execute(request); if (response.StatusCode == HttpStatusCode.Forbidden) { @@ -378,17 +407,17 @@ private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSett response = _httpClient.Execute(request); } + + return response.Content; } catch (HttpException ex) { throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); } - catch (WebException ex) + catch (Exception ex) { throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex); } - - return response.Content; } private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index 8d157dc30..2a74d2c9b 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -14,6 +14,13 @@ public QBittorrentSettingsValidator() RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace()); + RuleFor(c => c.Username).Empty() + .WithMessage("Username must be empty when using API Key.") + .When(c => c.ApiKey.IsNotNullOrWhiteSpace()); + RuleFor(c => c.Password).Empty() + .WithMessage("Password must be empty when using API Key.") + .When(c => c.ApiKey.IsNotNullOrWhiteSpace()); + RuleFor(c => c.Category).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'"); } } @@ -43,28 +50,31 @@ public QBittorrentSettings() [FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/api")] public string UrlBase { get; set; } - [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] + [FieldDefinition(4, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)] + public string ApiKey { get; set; } + + [FieldDefinition(5, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] public string Username { get; set; } - [FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] + [FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] + [FieldDefinition(7, Label = "DefaultCategory", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsDefaultCategoryHelpText")] public string Category { get; set; } - [FieldDefinition(7, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] + [FieldDefinition(8, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsPriorityItemHelpText")] public int Priority { get; set; } - [FieldDefinition(8, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "DownloadClientQbittorrentSettingsInitialStateHelpText")] + [FieldDefinition(9, Label = "DownloadClientSettingsInitialState", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "DownloadClientQbittorrentSettingsInitialStateHelpText")] public int InitialState { get; set; } - [FieldDefinition(9, Label = "DownloadClientQbittorrentSettingsSequentialOrder", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsSequentialOrderHelpText")] + [FieldDefinition(10, Label = "DownloadClientQbittorrentSettingsSequentialOrder", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsSequentialOrderHelpText")] public bool SequentialOrder { get; set; } - [FieldDefinition(10, Label = "DownloadClientQbittorrentSettingsFirstAndLastFirst", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText")] + [FieldDefinition(11, Label = "DownloadClientQbittorrentSettingsFirstAndLastFirst", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText")] public bool FirstAndLast { get; set; } - [FieldDefinition(11, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")] + [FieldDefinition(12, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")] public int ContentLayout { get; set; } public NzbDroneValidationResult Validate()