Skip to content

Commit

Permalink
Add cached client (#11)
Browse files Browse the repository at this point in the history
AbuseIPDB is an excellent service to add a caching layer in front.
With the free tier you have 1,000 checks per day - checking the same IP over and over will quickly exhaust these.

- #8 Add cached client utilizing a memory cache based on LazyCache.
The cached client is added in it's own package.

- #9 Add Readme.md to packages
nuget.org supports [Readme's in nuget packages](https://devblogs.microsoft.com/nuget/add-a-readme-to-your-nuget-package/).
Go ahead and bundle the readme with both packages.

- #10 Extend unittest coverage
Add more extensive unit tests.
  • Loading branch information
Kencdk authored Jun 2, 2021
1 parent 9d5e7c6 commit 9dcd02f
Show file tree
Hide file tree
Showing 17 changed files with 969 additions and 19 deletions.
113 changes: 112 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,113 @@
# kenc.abuseipdb
Wrapper around abuseipdb.com API
Wrapper around abuseipdb.com API for .net

## Getting started

```Powershell
PM> Install-Package Kenc.AbuseIPDB
```

Kenc.AbuseIPDb is built with dependency-injection as a first-class-citizen.
As a result, there's a helper function to register the library including pointing to the configuration section, if IConfiguration is being utilized.

```C#
services.AddAbuseIPDBClient(Configuration.GetSection("AbuseIPDB"));
```

with the configuration section "AbuseIPDB" having the following settings:
```JSON
"AbuseIPDB": {
"APIKey": "<APIKey>",
"APIEndpoint": "https://api.abuseipdb.com/api/v2/"
}
```

_Note_: Don't embed your API key with your source code, load it from [keyvault](https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-5.0) or another secure storage.

### Using the client
With the client registered with dependency injection, add IAbuseIPDBClient abuseIPDBClient to your constructor, eg:
```C#
private readonly ILogger<HomeController> _logger;
private readonly IAbuseIPDBClient _abuseIPDBClient;
private readonly IHttpContextAccessor _httpContextAccessor;

public HomeController(ILogger<HomeController> logger, IAbuseIPDBClient abuseIPDBClient, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_abuseIPDBClient = abuseIPDBClient ?? throw new ArgumentNullException(nameof(abuseIPDBClient));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
```

In an ASP.net MVC app, to check for an abusive IP:
```C#
[HttpPost]
public async Task<IActionResult> PostComment(CommentModel comment)
{
var ip = _httpContextAccessor.HttpContext.Features.Get<IHttpConnectionFeature>()?.RemoteIpAddress;
try
{
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
(Check abuseCheck, _) = await _abuseIPDBClient.CheckAsync(ip.ToString(), 90, false, cancellationTokenSource.Token);

if (abuseCheck.AbuseConfidenceScore > 70)
{
_logger.LogWarning($"{nameof(HomeController)} refused comment by {ip} due to high abuse confidence score.");
var error = new ErrorModel
{
Title = "Bad Request.",
Detail = "User IP is blocked due to abuse.",
Instance = $"/comment/{Activity.Current?.Id ?? httpContextAccessor.HttpContext.TraceIdentifier}",
Status = 400,
Type = "/comment/abusiveip"
};

// return a 400 with the error information
return Unauthorized(error);
}
}
catch (OperationCanceledException)
{
_logger.LogError($"{nameof(HomeController)}: Failed to check AbuseIPDb within configured timeout.");
}
catch (ApiException abuseIPException)
{
_logger.LogError($"{nameof(HomeController)}: Caught error with AbuseIPDB: {abuseIPException.Message}");
}
}
```

## Cached client

In case you have a website with a significant amount of traffic, or where users are expected to send multiple requests, consider using the cached IAbuseIPDBClient. This adds a memory cache in-front, so only a single lookup per IP/block is made.

```Powershell
PM> Install-Package Kenc.AbuseIPDB
```

Register it with dependency injection using:

```C#
services.AddAbuseIPDBClientCache(Configuration.GetSection("AbuseIPDB"), Configuration.GetSection("AbuseIPDBCache"));
```

The configuration section can be used to configure for how long values are cached.

| Name | Default |
| ----------------------- | -------- |
| CheckCacheLifetime | 1 hour |
| CheckBlockCacheLifetime | 1 hour |
| BlackListCacheLifetime | 24 hours |

Configuration values are deserialized into TimeSpan. By default these follow [ISO 8601 durations](https://en.wikipedia.org/wiki/ISO_8601#Durations)

The following sets the configurations to keep Check() responses for 1 minute, CheckBlock() responses for 2 hours and 30 minutes and lastly BlackList() checks for 36 hours.
```json
{
"AbuseIPDBCache": {
"CheckCacheLifetime": "PT1M",
"CheckBlockCacheLifetime": "PT2H30M",
"BlackListCacheLifetime": "PT36H"
}
}
```
25 changes: 25 additions & 0 deletions src/Kenc.AbuseIPDB.Cache/CacheConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Kenc.AbuseIPDB.Cache
{
using System;

/// <summary>
/// Configuration of Abuse IP DB cache.
/// </summary>
public class CacheConfiguration
{
/// <summary>
/// Gets or sets how long to cache the results for Check operations.
/// </summary>
public TimeSpan CheckCacheLifetime { get; set; } = TimeSpan.FromMinutes(60);

/// <summary>
/// Gets or sets how long to cache the results for CheckBlock operations.
/// </summary>
public TimeSpan CheckBlockCacheLifetime { get; set; } = TimeSpan.FromMinutes(60);

/// <summary>
/// Gets or sets how long to cache the results for blacklists.
/// </summary>
public TimeSpan BlackListCacheLifetime { get; set; } = TimeSpan.FromHours(24);
}
}
184 changes: 184 additions & 0 deletions src/Kenc.AbuseIPDB.Cache/CachedAbuseIPDBClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
namespace Kenc.AbuseIPDB.Cache
{
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Kenc.AbuseIPDB.Entities;
using Kenc.AbuseIPDB.Replies;
using LazyCache;
using Microsoft.Extensions.Options;

/// <summary>
/// Cache layer around <see cref="IAbuseIPDBClient"/>.
/// </summary>
/// <inheritdoc/>
public class CachedAbuseIPDBClient : IAbuseIPDBClient
{
private readonly IAppCache cache;
private readonly IAbuseIPDBClient client;
private readonly IOptions<CacheConfiguration> cacheConfiguration;
private readonly ReaderWriterLockSlim rwl = new();

private RateLimit latestCheckRateLimit;
private RateLimit latestBlackListRateLimit;
private RateLimit latestCheckBlockRateLimit;

/// <summary>
/// Initializes a new instance of the <see cref="CachedAbuseIPDBClient"/> class.
/// </summary>
/// <param name="client">Client to use for outgoing connections.</param>
/// <param name="cacheConfiguration">Configuration of the cache.</param>
/// <param name="cache">Instance of IAppCache.</param>
internal CachedAbuseIPDBClient(IAbuseIPDBClient client, IOptions<CacheConfiguration> cacheConfiguration, IAppCache cache = null)
{
this.client = client ?? throw new ArgumentNullException(nameof(client));
this.cache = cache ?? new CachingService();

if (cacheConfiguration.Value == null)
{
throw new ArgumentNullException(nameof(cacheConfiguration));
}

this.cacheConfiguration = cacheConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="CachedAbuseIPDBClient"/> class.
/// </summary>
/// <param name="httpClient">HTTPClient to use for connections.</param>
/// <param name="clientConfiguration">Configuration of <see cref="AbuseIPDBClient"/>.</param>
/// <param name="cacheConfiguration">Configuration of cache.</param>
/// <param name="cache">Instance of IAppCache.</param>
public CachedAbuseIPDBClient(HttpClient httpClient, IOptions<AbuseIPDBClientSettings> clientConfiguration, IOptions<CacheConfiguration> cacheConfiguration, IAppCache cache = null)
{
this.cache = cache ?? new CachingService();
if (cacheConfiguration.Value == null)
{
throw new ArgumentNullException(nameof(cacheConfiguration));
}

this.cacheConfiguration = cacheConfiguration;
client = new AbuseIPDBClient(httpClient, clientConfiguration);
}

public async Task<(IReadOnlyList<BlackListEntry> Data, BlackListMetadata Metadata, RateLimit RateLimit)> BlackListAsync(int confidenceMinimum = 100, int limit = 10000, CancellationToken cancellationToken = default)
{
(IReadOnlyList<BlackListEntry> data, BlackListMetadata metadata) = await cache.GetOrAddAsync($"blacklist_{confidenceMinimum}_{limit}", async (item) =>
{
item.AbsoluteExpirationRelativeToNow = cacheConfiguration.Value.BlackListCacheLifetime;
(IReadOnlyList<BlackListEntry> data, BlackListMetadata metadata, RateLimit rateLimit) = await client.BlackListAsync(confidenceMinimum, limit, cancellationToken);

if (rwl.TryEnterWriteLock(1000))
{
try
{
latestBlackListRateLimit = rateLimit;
}
finally
{
rwl.ExitWriteLock();
}
}

return (data, metadata);
});


if (rwl.TryEnterReadLock(1000))
{
try
{
return (data, metadata, latestBlackListRateLimit);
}
finally
{
rwl.ExitReadLock();
}
}

return (data, metadata, null);
}

public async Task<(Check Data, RateLimit RateLimit)> CheckAsync(string ipAddress, int maxAgeInDays, bool verbose, CancellationToken cancellationToken)
{
Check data = await cache.GetOrAddAsync(ipAddress, async (item) =>
{
item.AbsoluteExpirationRelativeToNow = cacheConfiguration.Value.CheckCacheLifetime;
(Check Data, RateLimit rateLimit) = await client.CheckAsync(ipAddress, maxAgeInDays, verbose, cancellationToken);

if (rwl.TryEnterWriteLock(1000))
{
try
{
latestCheckRateLimit = rateLimit;
}
finally
{
rwl.ExitWriteLock();
}
}

return Data;
});


if (rwl.TryEnterReadLock(1000))
{
try
{
return (data, latestCheckRateLimit);
}
finally
{
rwl.ExitReadLock();
}
}

return (data, null);
}

public async Task<(CheckBlockData data, RateLimit rateLimit)> CheckBlockAsync(string ipBlock, int maxAgeInDays = 30, CancellationToken cancellationToken = default)
{
CheckBlockData data = await cache.GetOrAddAsync(ipBlock, async (item) =>
{
item.AbsoluteExpirationRelativeToNow = cacheConfiguration.Value.CheckBlockCacheLifetime;
(CheckBlockData Data, RateLimit rateLimit) = await client.CheckBlockAsync(ipBlock, maxAgeInDays, cancellationToken);

if (rwl.TryEnterWriteLock(1000))
{
try
{
latestCheckBlockRateLimit = rateLimit;
}
finally
{
rwl.ExitWriteLock();
}
}
return Data;
});


if (rwl.TryEnterReadLock(1000))
{
try
{
return (data, latestCheckBlockRateLimit);
}
finally
{
rwl.ExitReadLock();
}
}

return (data, null);
}

public Task<(ReportUpdate Data, RateLimit rateLimit)> ReportAsync(string ip, string comment, Category[] categories, CancellationToken cancellationToken = default)
{
return client.ReportAsync(ip, comment, categories, cancellationToken);
}
}
}
22 changes: 22 additions & 0 deletions src/Kenc.AbuseIPDB.Cache/Extensions/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Kenc.AbuseIPDB.Cache
{
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

public static class DependencyInjection
{
/// <summary>
/// Add <see cref="IAbuseIPDBClient"/> to dependency injection.
/// </summary>
/// <param name="serviceCollection">Service collection to add it to</param>
/// <param name="clientConfiguration">Configuration to parse <see cref="AbuseIPDBClientSettings"/> from.</param>
/// <param name="cacheConfiguration">Configuration to parse <see cref="CacheConfiguration"/> from.</param>
/// <returns>The service collection.</returns>
public static IServiceCollection AddAbuseIPDBClientCache(this IServiceCollection serviceCollection, IConfiguration clientConfiguration, IConfiguration cacheConfiguration)
{
return serviceCollection.AddSingleton<IAbuseIPDBClient, CachedAbuseIPDBClient>()
.Configure<AbuseIPDBClientSettings>(clientConfiguration)
.Configure<CacheConfiguration>(cacheConfiguration);
}
}
}
47 changes: 47 additions & 0 deletions src/Kenc.AbuseIPDB.Cache/Kenc.AbuseIPDB.Cache.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

<!-- nuget package properties -->
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>Kenc.AbuseIPDB.Cache</PackageId>
<PackageDescription>Cache for Kenc.AbuseIPDB based on LazyCache.</PackageDescription>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageProjectUrl>https://github.com/Kencdk/kenc.abuseipdb</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>

<PackageTags>AbuseIPDB</PackageTags>
<Copyright>2021 Ken Christensen</Copyright>
<Description>Cache for Kenc.AbuseIPDB based on LazyCache.</Description>

<!-- source link properties -->
<RepositoryType>Github</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<RepositoryUrl>https://github.com/Kencdk/Kenc.abuseipdb/</RepositoryUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="LazyCache" Version="2.1.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Kenc.AbuseIPDB\Kenc.AbuseIPDB.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Kenc.AbuseIPDB.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
Loading

0 comments on commit 9dcd02f

Please sign in to comment.