Skip to content

Commit

Permalink
Code refactoring and general library maintenance
Browse files Browse the repository at this point in the history
  • Loading branch information
akacdev committed Nov 25, 2023
1 parent 837afb4 commit f135b41
Show file tree
Hide file tree
Showing 17 changed files with 361 additions and 239 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Build and Release

env:
DOTNET_VERSION: '8.x'
NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json'
BUILD_DIRECTORY: '${{ github.workspace }}/build'

on:
push:
tags:
- 'v*.*.*'

jobs:
build-and-release:
runs-on: ubuntu-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v2

- name: Get Version
id: get_version
run: |
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Get Project Metadata
id: get_project_meta
run: |
name=$(echo '${{ github.repository }}' | cut -d '/' -f 2)
echo "name=${name}" >> $GITHUB_OUTPUT
echo "path=${name}/${name}.csproj" >> $GITHUB_OUTPUT
- name: Setup .NET
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore Packages
run: dotnet restore ${{ steps.get_project_meta.outputs.path }}

- name: Build Project
run: dotnet build ${{ steps.get_project_meta.outputs.path }} /p:ContinuousIntegrationBuild=true --no-restore --configuration Release

- name: Pack Project
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 }}

- name: Push Package
env:
NUGET_AUTH_TOKEN: ${{ secrets.NUGET_AUTH_TOKEN }}
run: dotnet nuget push ${{ env.BUILD_DIRECTORY }}/*.nupkg -k $NUGET_AUTH_TOKEN -s ${{ env.NUGET_SOURCE_URL }}

- name: Create Release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.get_version.outputs.tag }}
body: ${{ github.event.head_commit.message }}
files: '${{ env.BUILD_DIRECTORY }}/*'
72 changes: 26 additions & 46 deletions AlienVault/API.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
using AlienVault.Entities;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace AlienVault
{
public static class API
internal static class API
{
public const int MaxRetries = 3;
public const int RetryDelay = 1000 * 1;
public const int PreviewMaxLength = 500;
public const int RetryDelay = 1000;

public static async Task<HttpResponseMessage> Request
(
Expand All @@ -24,7 +20,7 @@ public static async Task<HttpResponseMessage> Request
object obj,
HttpStatusCode target = HttpStatusCode.OK,
JsonSerializerOptions options = null)
=> await Request(cl, method, url, new StringContent(JsonSerializer.Serialize(obj, options ?? Constants.EnumOptions), Encoding.UTF8, "application/json"), target);
=> await Request(cl, method, url, await obj.Serialize(options ?? Constants.EnumOptions), target);

public static async Task<HttpResponseMessage> Request
(
Expand All @@ -39,7 +35,7 @@ public static async Task<HttpResponseMessage> Request
HttpResponseMessage res = null;
while (retries < MaxRetries)
{
HttpRequestMessage req = new(method, url)
using HttpRequestMessage req = new(method, url)
{
Content = content
};
Expand All @@ -52,54 +48,38 @@ public static async Task<HttpResponseMessage> Request
else await Task.Delay(RetryDelay);
}

if (retries == MaxRetries) throw new AlienVaultException($"Failed to request {method} {url} {retries} times. Remote endpoint is returning an 'Internal Server Error' message.");
content?.Dispose();

if (!target.HasFlag(res.StatusCode))
{
string prefix = $"Failed to request {method} {url}, ";
string text = await res.Content.ReadAsStringAsync();
if (retries == MaxRetries)
throw new AlienVaultException($"Failed to request {method} {url} after {retries} attempts. The API is returning an Internal Server Error.");

MediaTypeHeaderValue contentType = res.Content.Headers.ContentType;
if (contentType is null) throw new AlienVaultException(string.Concat(prefix, "the 'Content-Type' header is missing."));
if (target.HasFlag(res.StatusCode)) return res;

bool isJson = contentType.MediaType.StartsWith("application/json", StringComparison.InvariantCultureIgnoreCase);
string prefix = $"Failed to request {method} {url}, ";

if (!isJson) throw new AlienVaultException(string.Concat(
prefix,
$"received status code {res.StatusCode} and Content-Type {contentType.MediaType}",
$"\nPreview: {text[..Math.Min(text.Length, PreviewMaxLength)]}"), text);
MediaTypeHeaderValue contentType = res.Content.Headers.ContentType
?? throw new AlienVaultException(string.Concat(prefix, "the \"Content-Type\" header is missing."), res);

ApiError error = await res.Deseralize<ApiError>();
if (error is null) throw new AlienVaultException(string.Concat(prefix, "parsed error object is a null."));
bool isJson = contentType.MediaType == "application/json";

bool hasStatus = !string.IsNullOrEmpty(error.Status);
bool hasDetail = !string.IsNullOrEmpty(error.Detail);
if (!isJson) throw new AlienVaultException(string.Concat(
prefix,
$"received status code {res.StatusCode} and Content-Type {contentType.MediaType}",
$"\nPreview: {await res.GetPreview()}"));

if (!hasStatus && !hasDetail) throw new AlienVaultException(string.Concat(prefix, "parsed error object is missing necessary properties."));
ApiError error = await res.Deseralize<ApiError>()
?? throw new AlienVaultException(string.Concat(prefix, "failed to parse the error object"), res);

throw new AlienVaultException(string.Concat(
prefix,
"operation resulted in the following API error:",
hasStatus ? $"\nStatus: {error.Status}" : "",
hasDetail ? $"\nDetail: {error.Detail}" : ""), text);
}
bool hasStatus = !string.IsNullOrEmpty(error.Status);
bool hasDetail = !string.IsNullOrEmpty(error.Detail);

return res;
}

public static async Task<T> Deseralize<T>(this HttpResponseMessage res)
{
string json = await res.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(json)) throw new AlienVaultException("Response content is empty, can't parse as JSON.");
if (!hasStatus && !hasDetail) throw new AlienVaultException(string.Concat(prefix, "parsed error object is missing necessary properties."));

try
{
return JsonSerializer.Deserialize<T>(json, Constants.EnumOptions);
}
catch (Exception ex)
{
throw new AlienVaultException($"Exception while parsing JSON: {ex.GetType().Name} => {ex.Message}\nJSON preview: {json[..Math.Min(json.Length, PreviewMaxLength)]}");
}
throw new AlienVaultException(string.Concat(
prefix,
"operation resulted in the following API error:",
hasStatus ? $"\nStatus: {error.Status}" : "",
hasDetail ? $"\nDetail: {error.Detail}" : ""));
}
}
}
18 changes: 6 additions & 12 deletions AlienVault/AlienVault.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading;

namespace AlienVault
{
Expand All @@ -12,11 +11,6 @@ namespace AlienVault
/// </summary>
public class AlienVaultClient
{
/// <summary>
/// The base URI to use when communicating.
/// </summary>
public static readonly Uri BaseUri = new($"https://otx.alienvault.com/api/v{Constants.Version}/");

private static readonly HttpClientHandler HttpHandler = new()
{
AutomaticDecompression = DecompressionMethods.All,
Expand All @@ -25,9 +19,9 @@ public class AlienVaultClient

private readonly HttpClient Client = new(HttpHandler)
{
BaseAddress = BaseUri,
DefaultRequestVersion = new(2, 0),
Timeout = TimeSpan.FromMinutes(5)
BaseAddress = Constants.BaseUri,
DefaultRequestVersion = Constants.HttpVersion,
Timeout = Constants.Timeout
};

private readonly AlienVaultClientConfig Config;
Expand All @@ -40,7 +34,7 @@ public class AlienVaultClient
public AlienVaultClient(AlienVaultClientConfig config)
{
if (config is null) throw new ArgumentNullException(nameof(config), "Provided AlienVault Client config is null.");
if (string.IsNullOrEmpty(config.Key)) throw new ArgumentNullException(nameof(config.Key), "API key is null or empty.");
if (string.IsNullOrEmpty(config.Key)) throw new ArgumentNullException(nameof(config), "API key is null or empty.");

Config = config;

Expand All @@ -51,10 +45,10 @@ public AlienVaultClient(AlienVaultClientConfig config)
Client.DefaultRequestHeaders.Accept.ParseAdd("*/*");
Client.DefaultRequestHeaders.Add("X-OTX-API-Key", config.Key);

Users = new(Client, Config);
Users = new(Client);
Search = new(Client, Config);
Pulses = new(Client, Config);
Indicators = new(Client, Config);
Indicators = new(Client);
Data = new(Client, Config);
Analysis = new(Client, Config);
}
Expand Down
9 changes: 4 additions & 5 deletions AlienVault/AlienVault.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

<PropertyGroup>
<!--Basic Information-->
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<PackageId>AlienVault</PackageId>
<Product>AlienVault</Product>
<Authors>akac</Authors>
<Company>akac</Company>
<Description>An async C# library for interacting with the AlienVault OTX DirectConnect APIs.</Description>
<PackageTags>alienvault; otx; open-threat-exchange; alienvault-otx; directconnect; ioc; indicator; pulse; threat; intelligence; url; domain; malware; phishing</PackageTags>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README_NuGet.md</PackageReadmeFile>
<PackageReadmeFile>NuGet.md</PackageReadmeFile>

<!--Version-->
<Version>1.0.2</Version>
Expand All @@ -32,7 +32,6 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RequireLicenseAcceptance>false</RequireLicenseAcceptance>
<NeutralLanguage>en</NeutralLanguage>

</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
Expand All @@ -48,10 +47,10 @@
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="README_NuGet.md">
<None Include="NuGet.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>

</Project>
</Project>
26 changes: 24 additions & 2 deletions AlienVault/Constants.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AlienVault
Expand All @@ -10,18 +11,39 @@ internal static class Constants
/// </summary>
public const int Version = 1;
/// <summary>
/// The base URI to send requests to.
/// </summary>
public static readonly Uri BaseUri = new($"https://otx.alienvault.com/api/v{Version}/");
/// <summary>
/// The preferred HTTP request version to use.
/// </summary>
public static readonly Version HttpVersion = new(2, 0);
/// <summary>
/// The maximum delay before considering a request timeout.
/// </summary>
public static readonly TimeSpan Timeout = TimeSpan.FromMinutes(1);
/// <summary>
/// The value of the <c>User-Agent</c> header to send.
/// </summary>
public const string UserAgent = "OTX AlienVault C# Client - actually-akac/AlienVault";
/// <summary>
/// The maximum string length when displaying a preview of a response body.
/// </summary>
public const int PreviewMaxLength = 500;

/// <summary>
/// JSON serializer options used to serialize enums as snake case.
/// </summary>
public static readonly JsonSerializerOptions EnumOptions = new()
{
Converters =
{
new JsonStringEnumConverter(new SnakeCaseNamingPolicy())
}
};

/// <summary>
/// JSON serializer options used to serialize indicator enums.
/// </summary>
public static readonly JsonSerializerOptions IndicatorOptions = new()
{
Converters =
Expand Down
7 changes: 2 additions & 5 deletions AlienVault/Entities/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ public class AlienVaultClientConfig
/// <summary>
/// Create a new instance of the configuration class.
/// </summary>
public AlienVaultClientConfig()
{

}
public AlienVaultClientConfig() { }

/// <summary>
/// Your AlienVault API key. Get one at <a href="https://otx.alienvault.com/settings">https://otx.alienvault.com/settings</a>.
Expand All @@ -24,4 +21,4 @@ public AlienVaultClientConfig()
/// </summary>
public bool StrictLimit { get; set; }
}
}
}
9 changes: 2 additions & 7 deletions AlienVault/Entities/Errors.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Text.Json.Serialization;

namespace AlienVault.Entities
{
Expand All @@ -15,4 +10,4 @@ public class ApiError
[JsonPropertyName("detail")]
public string Detail { get; set; }
}
}
}
2 changes: 1 addition & 1 deletion AlienVault/Entities/Indicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ public class Validation
[JsonPropertyName("name")]
public string Name { get; set; }
}
}
}
Loading

0 comments on commit f135b41

Please sign in to comment.