From 2a49824ab7522d4226e5c60e85d1ca22d9f8e9d8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 4 Mar 2019 23:41:46 -0500 Subject: [PATCH] BlockIpHostedService to replace func --- src/Admin/AdminSettings.cs | 8 + .../HostedServices/BlockIpHostedService.cs | 234 ++++++++++++++++++ src/Admin/Startup.cs | 4 + src/Admin/appsettings.json | 7 +- 4 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/Admin/HostedServices/BlockIpHostedService.cs diff --git a/src/Admin/AdminSettings.cs b/src/Admin/AdminSettings.cs index 031c059b3c..2eecc3a490 100644 --- a/src/Admin/AdminSettings.cs +++ b/src/Admin/AdminSettings.cs @@ -3,5 +3,13 @@ public class AdminSettings { public virtual string Admins { get; set; } + public virtual CloudflareSettings Cloudflare { get; set; } + + public class CloudflareSettings + { + public string ZoneId { get; set; } + public string AuthEmail { get; set; } + public string AuthKey { get; set; } + } } } diff --git a/src/Admin/HostedServices/BlockIpHostedService.cs b/src/Admin/HostedServices/BlockIpHostedService.cs new file mode 100644 index 0000000000..4678512b15 --- /dev/null +++ b/src/Admin/HostedServices/BlockIpHostedService.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Bit.Core; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Queue; +using Newtonsoft.Json; + +namespace Bit.Admin.HostedServices +{ + public class BlockIpHostedService : IHostedService, IDisposable + { + private readonly ILogger _logger; + private readonly GlobalSettings _globalSettings; + public readonly AdminSettings _adminSettings; + + private Task _executingTask; + private CancellationTokenSource _cts; + private CloudQueue _blockQueue; + private CloudQueue _unblockQueue; + private HttpClient _httpClient = new HttpClient(); + + public BlockIpHostedService( + ILogger logger, + IOptions adminSettings, + GlobalSettings globalSettings) + { + _logger = logger; + _globalSettings = globalSettings; + _adminSettings = adminSettings?.Value; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _executingTask = ExecuteAsync(_cts.Token); + return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if(_executingTask == null) + { + return; + } + _cts.Cancel(); + await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken)); + cancellationToken.ThrowIfCancellationRequested(); + } + + public void Dispose() + { } + + private async Task ExecuteAsync(CancellationToken cancellationToken) + { + var storageAccount = CloudStorageAccount.Parse(_globalSettings.Storage.ConnectionString); + var queueClient = storageAccount.CreateCloudQueueClient(); + _blockQueue = queueClient.GetQueueReference("blockip"); + _unblockQueue = queueClient.GetQueueReference("unblockip"); + + while(!cancellationToken.IsCancellationRequested) + { + var blockMessages = await _blockQueue.GetMessagesAsync(32, TimeSpan.FromSeconds(15), + null, null, cancellationToken); + if(blockMessages.Any()) + { + foreach(var message in blockMessages) + { + try + { + await BlockIpAsync(message.AsString); + } + catch(Exception e) + { + _logger.LogError(e, "Failed to block IP."); + } + await _blockQueue.DeleteMessageAsync(message); + } + } + + var unblockMessages = await _unblockQueue.GetMessagesAsync(32, TimeSpan.FromSeconds(15), + null, null, cancellationToken); + if(unblockMessages.Any()) + { + foreach(var message in unblockMessages) + { + try + { + await UnblockIpAsync(message.AsString); + } + catch(Exception e) + { + _logger.LogError(e, "Failed to unblock IP."); + } + await _unblockQueue.DeleteMessageAsync(message); + } + } + + await Task.Delay(TimeSpan.FromSeconds(5)); + } + } + + private async Task BlockIpAsync(string message) + { + var request = new HttpRequestMessage(); + request.Headers.Accept.Clear(); + request.Headers.Add("X-Auth-Email", _adminSettings.Cloudflare.AuthEmail); + request.Headers.Add("X-Auth-Key", _adminSettings.Cloudflare.AuthKey); + request.Method = HttpMethod.Post; + request.RequestUri = new Uri("https://api.cloudflare.com/" + + $"client/v4/zones/{_adminSettings.Cloudflare.ZoneId}/firewall/access_rules/rules"); + + var bodyContent = JsonConvert.SerializeObject(new + { + mode = "block", + configuration = new + { + target = "ip", + value = message + }, + notes = $"Rate limit abuse on {DateTime.UtcNow.ToString()}." + }); + request.Content = new StringContent(bodyContent, Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(request); + if(!response.IsSuccessStatusCode) + { + return; + } + + var responseString = await response.Content.ReadAsStringAsync(); + var accessRuleResponse = JsonConvert.DeserializeObject(responseString); + if(!accessRuleResponse.Success) + { + return; + } + + // TODO: Send `accessRuleResponse.Result?.Id` message to unblock queue + } + + private async Task UnblockIpAsync(string message) + { + if(string.IsNullOrWhiteSpace(message)) + { + return; + } + + if(message.Contains(".") || message.Contains(":")) + { + // IP address messages + var request = new HttpRequestMessage(); + request.Headers.Accept.Clear(); + request.Headers.Add("X-Auth-Email", _adminSettings.Cloudflare.AuthEmail); + request.Headers.Add("X-Auth-Key", _adminSettings.Cloudflare.AuthKey); + request.Method = HttpMethod.Get; + request.RequestUri = new Uri("https://api.cloudflare.com/" + + $"client/v4/zones/{_adminSettings.Cloudflare.ZoneId}/firewall/access_rules/rules?" + + $"configuration_target=ip&configuration_value={message}"); + + var response = await _httpClient.SendAsync(request); + if(!response.IsSuccessStatusCode) + { + return; + } + + var responseString = await response.Content.ReadAsStringAsync(); + var listResponse = JsonConvert.DeserializeObject(responseString); + if(!listResponse.Success) + { + return; + } + + foreach(var rule in listResponse.Result) + { + if(rule.Configuration?.Value != message) + { + continue; + } + + await DeleteAccessRuleAsync(rule.Id); + } + } + else + { + // Rule Id messages + await DeleteAccessRuleAsync(message); + } + } + + private async Task DeleteAccessRuleAsync(string ruleId) + { + var request = new HttpRequestMessage(); + request.Headers.Accept.Clear(); + request.Headers.Add("X-Auth-Email", _adminSettings.Cloudflare.AuthEmail); + request.Headers.Add("X-Auth-Key", _adminSettings.Cloudflare.AuthKey); + request.Method = HttpMethod.Delete; + request.RequestUri = new Uri("https://api.cloudflare.com/" + + $"client/v4/zones/{_adminSettings.Cloudflare.ZoneId}/firewall/access_rules/rules/{ruleId}"); + await _httpClient.SendAsync(request); + } + + public class ListResponse + { + public bool Success { get; set; } + public List Result { get; set; } + } + + public class AccessRuleResponse + { + public bool Success { get; set; } + public AccessRuleResultResponse Result { get; set; } + } + + public class AccessRuleResultResponse + { + public string Id { get; set; } + public string Notes { get; set; } + public ConfigurationResponse Configuration { get; set; } + + public class ConfigurationResponse + { + public string Target { get; set; } + public string Value { get; set; } + } + } + } +} diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 6248da8da6..6ad519531a 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -75,6 +75,10 @@ namespace Bit.Admin // Jobs service Jobs.JobsHostedService.AddJobsServices(services); services.AddHostedService(); + if(!globalSettings.SelfHosted) + { + services.AddHostedService(); + } } public void Configure( diff --git a/src/Admin/appsettings.json b/src/Admin/appsettings.json index 8c66947b49..5221d0a716 100644 --- a/src/Admin/appsettings.json +++ b/src/Admin/appsettings.json @@ -45,7 +45,12 @@ } }, "adminSettings": { - "admins": "" + "admins": "", + "cloudflare": { + "zoneId": "SECRET", + "authEmail": "SECRET", + "authKey": "SECRET" + } }, "braintree": { "production": false,