Skip to content

Commit e38bb22

Browse files
committed
Code refactoring and maintenance, add Github Actions
1 parent bae58f7 commit e38bb22

11 files changed

Lines changed: 253 additions & 175 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Build and Release
2+
3+
env:
4+
DOTNET_VERSION: '8.x'
5+
NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json'
6+
BUILD_DIRECTORY: '${{ github.workspace }}/build'
7+
8+
on:
9+
push:
10+
tags:
11+
- 'v*.*.*'
12+
13+
jobs:
14+
build-and-release:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout Repository
19+
uses: actions/checkout@v2
20+
21+
- name: Get Version
22+
id: get_version
23+
run: |
24+
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
25+
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
26+
27+
- name: Get Project Metadata
28+
id: get_project_meta
29+
run: |
30+
name=$(echo '${{ github.repository }}' | cut -d '/' -f 2)
31+
32+
echo "name=${name}" >> $GITHUB_OUTPUT
33+
echo "path=${name}/${name}.csproj" >> $GITHUB_OUTPUT
34+
35+
- name: Setup .NET
36+
uses: actions/setup-dotnet@v3.2.0
37+
with:
38+
dotnet-version: ${{ env.DOTNET_VERSION }}
39+
40+
- name: Restore Packages
41+
run: dotnet restore ${{ steps.get_project_meta.outputs.path }}
42+
43+
- name: Build Project
44+
run: dotnet build ${{ steps.get_project_meta.outputs.path }} /p:ContinuousIntegrationBuild=true --no-restore --configuration Release
45+
46+
- name: Pack Project
47+
run: dotnet pack ${{ steps.get_project_meta.outputs.path }} --no-restore --no-build --configuration Release --include-symbols -p:PackageVersion=${{ steps.get_version.outputs.version }} --output ${{ env.BUILD_DIRECTORY }}
48+
49+
- name: Push Package
50+
env:
51+
NUGET_AUTH_TOKEN: ${{ secrets.NUGET_AUTH_TOKEN }}
52+
run: dotnet nuget push ${{ env.BUILD_DIRECTORY }}/*.nupkg -k $NUGET_AUTH_TOKEN -s ${{ env.NUGET_SOURCE_URL }}
53+
54+
- name: Create Release
55+
uses: softprops/action-gh-release@v1
56+
with:
57+
token: ${{ secrets.GITHUB_TOKEN }}
58+
name: ${{ steps.get_version.outputs.tag }}
59+
body: ${{ github.event.head_commit.message }}
60+
files: '${{ env.BUILD_DIRECTORY }}/*'

AbuseIPDB/API.cs

Lines changed: 38 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,94 @@
1-
using System;
2-
using System.IO;
1+
using System.IO;
32
using System.Net;
43
using System.Net.Http;
54
using System.Net.Http.Headers;
65
using System.Text;
7-
using System.Text.Json;
86
using System.Threading.Tasks;
97

108
namespace AbuseIPDB
119
{
12-
public static class API
10+
internal static class API
1311
{
14-
public const int MaxRetries = 3;
15-
public const int RetryDelay = 1000 * 3;
16-
public const int ExtraDelay = 1000;
17-
public const int PreviewMaxLength = 500;
18-
1912
public static async Task<HttpResponseMessage> Request
2013
(
2114
this HttpClient cl,
2215
HttpMethod method,
23-
string url,
16+
string path,
2417
object obj,
25-
HttpStatusCode target = HttpStatusCode.OK,
2618
bool absoluteUrl = false)
27-
=> await Request(cl, method, url, new StringContent(JsonSerializer.Serialize(obj), Encoding.UTF8, "application/json"), target, absoluteUrl: absoluteUrl);
19+
=> await Request(cl, method, path, await obj.Serialize(), absoluteUrl: absoluteUrl);
2820

2921
public static async Task<HttpResponseMessage> Request
3022
(
3123
this HttpClient cl,
3224
HttpMethod method,
33-
string url,
25+
string path,
3426
Stream stream,
3527
string fieldName,
3628
string fileName,
37-
HttpStatusCode target = HttpStatusCode.OK,
3829
bool absoluteUrl = false)
39-
=> await Request(cl, method, url, new StreamContent(stream), target, fieldName, fileName, absoluteUrl);
30+
=> await Request(cl, method, path, new StreamContent(stream), fieldName, fileName, absoluteUrl);
4031

4132
public static async Task<HttpResponseMessage> Request
4233
(
4334
this HttpClient cl,
4435
HttpMethod method,
45-
string url,
36+
string path,
4637
HttpContent content = null,
47-
HttpStatusCode target = HttpStatusCode.OK,
4838
string fieldName = null,
4939
string fileName = null,
5040
bool absoluteUrl = false)
5141
{
52-
int retries = 0;
53-
54-
HttpResponseMessage res = null;
42+
using HttpRequestMessage req = new(method, absoluteUrl ? path : string.Concat(Constants.BaseUri, path));
5543

56-
while (res is null || !target.HasFlag(res.StatusCode) && retries < MaxRetries)
44+
if (!string.IsNullOrEmpty(fieldName) && !string.IsNullOrEmpty(fileName))
5745
{
58-
HttpRequestMessage req = new(method, absoluteUrl ? url : string.Concat(AbuseIPDBClient.BaseUri, url));
59-
60-
if (content is not StreamContent) req.Content = content;
61-
else req.Content = new MultipartFormDataContent()
46+
req.Content = new MultipartFormDataContent()
6247
{
6348
{ content, fieldName, fileName }
6449
};
65-
66-
res = await cl.SendAsync(req);
67-
68-
MediaTypeHeaderValue contentType = res.Content.Headers.ContentType;
69-
if (!absoluteUrl && contentType.MediaType != "application/json")
70-
{
71-
bool includePreview = contentType.MediaType.StartsWith("text/");
72-
string preview = null;
73-
74-
if (includePreview)
75-
{
76-
string data = await res.Content.ReadAsStringAsync();
77-
preview = $"\nPreview: {data[..Math.Min(data.Length, PreviewMaxLength)]}";
78-
}
79-
80-
throw new AbuseIPDBException($"Expected response to be JSON, but received '{contentType.MediaType}'{preview}");
81-
}
82-
83-
if (!target.HasFlag(res.StatusCode) && (int)res.StatusCode >= 500) await Task.Delay(RetryDelay);
84-
else break;
85-
86-
retries++;
8750
}
51+
else req.Content = content;
8852

89-
if (!target.HasFlag(res.StatusCode))
90-
{
91-
AbuseIPDBError[] errors = (await res.Deseralize<AbuseIPDBErrorContainer>())?.Errors;
92-
if (errors is null) throw new AbuseIPDBException($"Failed to request {method} {url}, expected one of the following status codes: {string.Join(", ", res.StatusCode.GetFlags())} but received {res.StatusCode}");
53+
HttpResponseMessage res = await cl.SendAsync(req);
9354

94-
string suffix = errors.Length == 1 ? "" : "s";
55+
MediaTypeHeaderValue contentType = res.Content.Headers.ContentType;
56+
if (!absoluteUrl && contentType.MediaType != "application/json")
57+
{
58+
bool includePreview = contentType.MediaType.StartsWith("text/");
59+
string preview = includePreview ? $"\nPreview: {await res.GetPreview()}" : null;
9560

96-
StringBuilder sb = new();
97-
sb.AppendLine($"Failed to request {method} {url}, received {errors.Length} API error{suffix}.");
98-
for (int i = 0; i < errors.Length; i++)
99-
{
100-
AbuseIPDBError error = errors[i];
101-
error.Index = i;
102-
sb.Append($"[#{i + 1}] (Status Code: {error.Status}) {error.Detail}");
61+
throw new AbuseIPDBException($"Expected response to be JSON, but received '{contentType.MediaType}'{preview}");
62+
}
10363

104-
if (error.Source is null || error.Source.Parameter is null) { sb.AppendLine(); continue; }
105-
sb.AppendLine($", source parameter: {error.Source.Parameter}");
106-
}
64+
if (res.IsSuccessStatusCode) return res;
10765

108-
AbuseIPDBException ex = new(sb.ToString(), errors);
66+
AbuseIPDBError[] errors = ((await res.Deseralize<AbuseIPDBErrorContainer>())?.Errors)
67+
?? throw new AbuseIPDBException($"Failed to request {method} {path}, received status code {res.StatusCode}");
10968

110-
if (res.StatusCode == HttpStatusCode.TooManyRequests)
111-
{
112-
RetryConditionHeaderValue retry = res.Headers.RetryAfter;
113-
if (retry is not null) ex.RetryAfter = (int)retry.Delta.Value.TotalMilliseconds;
114-
}
69+
string suffix = errors.Length == 1 ? "" : "s";
70+
StringBuilder sb = new();
71+
72+
sb.AppendLine($"Failed to request {method} {path}, received {errors.Length} API error{suffix}.");
73+
for (int i = 0; i < errors.Length; i++)
74+
{
75+
AbuseIPDBError error = errors[i];
76+
error.Index = i;
77+
sb.Append($"[#{i + 1}] (status code: {error.Status}) {error.Detail}");
11578

116-
throw ex;
79+
if (error.Source is null || error.Source.Parameter is null) { sb.AppendLine(); continue; }
80+
sb.AppendLine($", source parameter: {error.Source.Parameter}");
11781
}
11882

119-
return res;
120-
}
121-
122-
public static async Task<T> Deseralize<T>(this HttpResponseMessage res, JsonSerializerOptions options = null)
123-
{
124-
Stream stream = await res.Content.ReadAsStreamAsync();
125-
if (stream.Length == 0) throw new AbuseIPDBException("Response content is empty, can't parse as JSON.");
83+
AbuseIPDBException ex = new(sb.ToString(), errors);
12684

127-
try
85+
if (res.StatusCode == HttpStatusCode.TooManyRequests)
12886
{
129-
return await JsonSerializer.DeserializeAsync<T>(stream, options);
87+
RetryConditionHeaderValue retry = res.Headers.RetryAfter;
88+
if (retry is not null) ex.RetryAfter = (int)retry.Delta.Value.TotalMilliseconds;
13089
}
131-
catch (Exception ex)
132-
{
133-
using StreamReader sr = new(stream);
134-
string text = await sr.ReadToEndAsync();
13590

136-
throw new AbuseIPDBException($"Exception while parsing JSON: {ex.GetType().Name} => {ex.Message}\nPreview: {text[..Math.Min(text.Length, PreviewMaxLength)]}");
137-
}
91+
throw ex;
13892
}
13993
}
14094
}

AbuseIPDB/AbuseIPDB.cs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,19 @@
1010
namespace AbuseIPDB
1111
{
1212
/// <summary>
13-
/// The main class for interacting with he API. Create an instance of it by calling a constructor.
13+
/// The primary class for interacting with he API. Create an instance of it by calling a constructor.
1414
/// </summary>
1515
public class AbuseIPDBClient
1616
{
17-
/// <summary>
18-
/// Base URI to send requests to.
19-
/// </summary>
20-
public static readonly Uri BaseUri = new($"https://api.abuseipdb.com/api/v{Constants.Version}/");
21-
2217
private static readonly HttpClientHandler HttpHandler = new()
2318
{
2419
AutomaticDecompression = DecompressionMethods.All
2520
};
2621

2722
private readonly HttpClient Client = new(HttpHandler)
2823
{
29-
BaseAddress = BaseUri,
30-
DefaultRequestVersion = new(2, 0)
24+
BaseAddress = Constants.BaseUri,
25+
DefaultRequestVersion = Constants.HttpVersion
3126
};
3227

3328
private readonly AbuseIPDBClientConfig Config;
@@ -39,23 +34,24 @@ public class AbuseIPDBClient
3934
/// <exception cref="ArgumentNullException"></exception>
4035
public AbuseIPDBClient(string key)
4136
{
37+
if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key), "API key is null or empty.");
38+
4239
Config = new()
4340
{
4441
Key = key
4542
};
4643

47-
if (string.IsNullOrEmpty(Config.Key)) throw new ArgumentNullException(nameof(key), "An empty or null API Key was provided.");
48-
4944
InitializeClient();
5045
}
5146

5247
/// <summary>
5348
/// Create a new instance of the client for interacting with the API by passing an advanced <see cref="AbuseIPDBClientConfig"/> configuration object.
5449
/// </summary>
55-
/// <param name="config"></param>
50+
/// <param name="config">The configuration object with at least a key specified.</param>
5651
public AbuseIPDBClient(AbuseIPDBClientConfig config)
5752
{
5853
if (config is null) throw new ArgumentNullException(nameof(config), "Client config object is null.");
54+
if (string.IsNullOrEmpty(config.Key)) throw new ArgumentNullException(nameof(config), "API key is null or empty.");
5955
Config = config;
6056

6157
InitializeClient();
@@ -84,7 +80,7 @@ public async Task<CheckedIP> Check(string ip, bool verbose = true, int maxAge =
8480

8581
HttpResponseMessage res = await Client.Request(
8682
HttpMethod.Get,
87-
$"check?ipAddress={HttpUtility.UrlEncode(ip)}&maxAgeInDays={maxAge}{(verbose ? "&verbose" : "")}");
83+
$"check?ipAddress={ip.UrlEncode()}&maxAgeInDays={maxAge}{(verbose ? "&verbose" : "")}");
8884

8985
return (await res.Deseralize<CheckedIPContainer>()).Data;
9086
}
@@ -114,9 +110,7 @@ public async Task<IPReport[]> GetReports(string ip, int limit = 100, int maxAge
114110
{
115111
HttpResponseMessage res = await Client.Request(
116112
HttpMethod.Get,
117-
$"reports?ipAddress={HttpUtility.UrlEncode(ip)}&maxAgeInDays={maxAge}&page={pageNumber}&perPage={perPage}",
118-
target: HttpStatusCode.OK
119-
);
113+
$"reports?ipAddress={HttpUtility.UrlEncode(ip)}&maxAgeInDays={maxAge}&page={pageNumber}&perPage={perPage}");
120114

121115
IPReportsPage page = (await res.Deseralize<IPReportsContainer>()).Data;
122116

@@ -176,7 +170,8 @@ public async Task<BlacklistedIP[]> GetBlacklist(
176170
string[] exceptCountries = null)
177171
{
178172
if (limit <= 0) throw new ArgumentOutOfRangeException(nameof(limit), "Limit has to be a positive value.");
179-
if (confidenceMinimum.HasValue && (confidenceMinimum.Value < 0 || confidenceMinimum.Value > 100)) throw new ArgumentOutOfRangeException(nameof(confidenceMinimum), "Minimum confidence score has to be a valid percentage value.");
173+
if (confidenceMinimum.HasValue && (confidenceMinimum.Value < 0 || confidenceMinimum.Value > 100))
174+
throw new ArgumentOutOfRangeException(nameof(confidenceMinimum), "Minimum confidence score has to be a valid percentage value.");
180175

181176
HttpResponseMessage res = await Client.Request(
182177
HttpMethod.Get,
@@ -204,6 +199,7 @@ public async Task<BlacklistedIP[]> GetBlacklist(
204199
public async Task<ReportedIP> Report(string ip, IPReportCategory[] categories, string comment)
205200
{
206201
if (string.IsNullOrEmpty(ip)) throw new ArgumentNullException(nameof(ip), "IP address to use is null or empty.");
202+
if (categories is null) throw new ArgumentNullException(nameof(categories), "Categories are null.");
207203

208204
HttpResponseMessage res = await Client.Request(
209205
HttpMethod.Post,

AbuseIPDB/AbuseIPDB.csproj

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<!--Basic Information-->
5-
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
5+
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
66
<PackageId>AbuseIPDB</PackageId>
77
<Product>AbuseIPDB</Product>
88
<Authors>akac</Authors>
@@ -27,12 +27,11 @@
2727
<IncludeSymbols>true</IncludeSymbols>
2828
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
2929
<GenerateDocumentationFile>true</GenerateDocumentationFile>
30-
30+
3131
<!--Miscellaneous-->
3232
<PackageLicenseExpression>MIT</PackageLicenseExpression>
3333
<RequireLicenseAcceptance>false</RequireLicenseAcceptance>
3434
<NeutralLanguage>en</NeutralLanguage>
35-
3635
</PropertyGroup>
3736

3837
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -44,10 +43,6 @@
4443
</PropertyGroup>
4544

4645
<ItemGroup>
47-
<None Include="icon.svg">
48-
<Pack>True</Pack>
49-
<PackagePath>\</PackagePath>
50-
</None>
5146
<None Include="icon.png">
5247
<Pack>True</Pack>
5348
<PackagePath>\</PackagePath>

0 commit comments

Comments
 (0)