Compare commits

16 Commits

30 changed files with 648 additions and 278 deletions

View File

@@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,93 +0,0 @@
using AS1024.GeoFeed.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
{
public class GeoFeedCacheService : IHostedService
{
private readonly ILogger<GeoFeedCacheService> logger;
private readonly IGeoFeedProvider feedProvider;
private readonly IHost host;
public GeoFeedCacheService(ILogger<GeoFeedCacheService> logger,
IGeoFeedProvider feedProvider,
IHost host)
{
this.logger = logger;
this.feedProvider = feedProvider;
this.host = host;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = StartPerioidicSync(cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public async Task<bool> StartPerioidicSync(CancellationToken Token)
{
await DBContextMigrate();
List<GeoFeedCacheEntry> geoFeedCacheEntry = [];
while (!Token.IsCancellationRequested)
{
logger.LogInformation("Running on disk fallback cache process...");
try
{
using var scope = host.Services.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<GeoFeedCacheDbContext>();
var results = await feedProvider.GetGeoFeedData();
results.ForEach(x =>
{
geoFeedCacheEntry.Add(new()
{
Prefix = x.Prefix,
GeolocCity = x.GeolocCity,
GeolocCountry = x.GeolocCountry,
GeolocHasLocation = x.GeolocHasLocation,
GeolocPostalCode = x.GeolocPostalCode,
GeolocRegion = x.GeolocRegion
});
});
if (dbContext.GeoFeedCacheEntries.Any())
{
dbContext.GeoFeedCacheEntries.RemoveRange(dbContext.GeoFeedCacheEntries.ToArray());
}
await dbContext.AddRangeAsync(geoFeedCacheEntry, Token);
await dbContext.SaveChangesAsync(Token);
}
catch (Exception ex)
{
logger.LogWarning("On disk cache failed to run. Waiting on 30 minutes before retry...");
}
await Task.Delay(TimeSpan.FromMinutes(30));
}
return false;
}
private async Task DBContextMigrate()
{
using IServiceScope scope = host.Services.CreateScope();
using GeoFeedCacheDbContext? dbContext =
scope.ServiceProvider.GetService<GeoFeedCacheDbContext>();
#pragma warning disable CS8602 // Dereference of a possibly null reference.
if (dbContext.Database.GetPendingMigrations().Any()) {
await dbContext.Database.MigrateAsync();
}
#pragma warning restore CS8602 // Dereference of a possibly null reference.
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedCacheDbContext : DbContext
{

View File

@@ -1,7 +1,7 @@
using AS1024.GeoFeed.Models;
using System.ComponentModel.DataAnnotations;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedCacheEntry : IPGeoFeed
{

View File

@@ -1,7 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace AS1024.GeoFeed.Core.GeoFeedLocalCache
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedDesignTimeMigration : IDesignTimeDbContextFactory<GeoFeedCacheDbContext>
{

View File

@@ -0,0 +1,72 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using AS1024.GeoFeed.Core.Tools;
namespace AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache
{
public class GeoFeedSqliteCache : IGeoFeedPersistentCacheProvider
{
protected readonly GeoFeedCacheDbContext dbContext;
private readonly IGeoFeedProvider feedProvider;
public GeoFeedSqliteCache(GeoFeedCacheDbContext geoFeedCacheDb,
IHost host,
IGeoFeedProvider provider)
{
dbContext = geoFeedCacheDb;
feedProvider = provider;
}
public string ProviderName => "sqlite";
public async Task<bool> CacheGeoFeed(IList<IPGeoFeed> pGeoFeeds)
{
await DBContextMigrate();
List<GeoFeedCacheEntry> geoFeedCacheEntry = [];
var results = pGeoFeeds.ToList();
results.ForEach(x =>
{
geoFeedCacheEntry.Add(new()
{
Prefix = x.Prefix,
GeolocCity = x.GeolocCity,
GeolocCountry = x.GeolocCountry,
GeolocHasLocation = x.GeolocHasLocation,
GeolocPostalCode = x.GeolocPostalCode,
GeolocRegion = x.GeolocRegion
});
});
if (dbContext.GeoFeedCacheEntries.Any())
{
dbContext.GeoFeedCacheEntries.RemoveRange(dbContext.GeoFeedCacheEntries.ToArray());
}
await dbContext.AddRangeAsync(geoFeedCacheEntry);
await dbContext.SaveChangesAsync();
return true;
}
public string GetGeoFeed()
{
var results =
dbContext.GeoFeedCacheEntries.ToList();
List<IPGeoFeed> cachedData = [];
results.ForEach(cachedData.Add);
return cachedData.ToGeoFeedCsv();
}
private async Task DBContextMigrate()
{
if (dbContext.Database.GetPendingMigrations().Any())
{
await dbContext.Database.MigrateAsync();
}
}
}
}

View File

@@ -1,6 +1,6 @@
// <auto-generated />
using System;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -8,10 +8,10 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AS1024.GeoFeed.Core.Migrations
namespace AS1024.GeoFeed.Core.SqliteGeoFeedCache.Migrations
{
[DbContext(typeof(GeoFeedCacheDbContext))]
[Migration("20240113204143_InitialMigration")]
[Migration("20240114002908_InitialMigration")]
partial class InitialMigration
{
/// <inheritdoc />
@@ -20,7 +20,7 @@ namespace AS1024.GeoFeed.Core.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedLocalCache.GeoFeedCacheEntry", b =>
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache.GeoFeedCacheEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View File

@@ -2,7 +2,7 @@
#nullable disable
namespace AS1024.GeoFeed.Core.Migrations
namespace AS1024.GeoFeed.Core.SqliteGeoFeedCache.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration

View File

@@ -1,13 +1,13 @@
// <auto-generated />
using System;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AS1024.GeoFeed.Core.Migrations
namespace AS1024.GeoFeed.Core.SqliteGeoFeedCache.Migrations
{
[DbContext(typeof(GeoFeedCacheDbContext))]
partial class GeoFeedCacheDbContextModelSnapshot : ModelSnapshot
@@ -17,7 +17,7 @@ namespace AS1024.GeoFeed.Core.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedLocalCache.GeoFeedCacheEntry", b =>
modelBuilder.Entity("AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache.GeoFeedCacheEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />

View File

@@ -0,0 +1,58 @@
using AS1024.GeoFeed.Core.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.Core.CacheService
{
public class GeoFeedCacheService : IHostedService
{
private readonly ILogger<GeoFeedCacheService> logger;
private readonly IGeoFeedProvider feedProvider;
private readonly IHost host;
public GeoFeedCacheService(ILogger<GeoFeedCacheService> logger,
IGeoFeedProvider feedProvider,
IHost host)
{
this.logger = logger;
this.feedProvider = feedProvider;
this.host = host;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = StartPerioidicSync(cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public async Task<bool> StartPerioidicSync(CancellationToken Token)
{
while (!Token.IsCancellationRequested)
{
try
{
var scope = host.Services.CreateScope();
var persistentCacheProvider =
scope.ServiceProvider.GetRequiredService<IGeoFeedPersistentCacheProvider>();
var results = await feedProvider.GetGeoFeedData();
await persistentCacheProvider.CacheGeoFeed(results);
}
catch (Exception)
{
logger.LogWarning("On disk cache failed to run. Waiting on 30 minutes before retry...");
}
await Task.Delay(TimeSpan.FromMinutes(30));
}
return false;
}
}
}

View File

@@ -0,0 +1,55 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using AS1024.GeoFeed.Core.Tools;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.Core.CacheService
{
public class GeoFeedLocalFileCache : IGeoFeedPersistentCacheProvider
{
private readonly IConfiguration configuration;
private readonly ILogger<GeoFeedLocalFileCache> logger;
public GeoFeedLocalFileCache(IConfiguration _configuration,
ILogger<GeoFeedLocalFileCache> logger)
{
configuration = _configuration;
this.logger = logger;
}
string IGeoFeedPersistentCacheProvider.ProviderName => "file";
Task<bool> IGeoFeedPersistentCacheProvider.CacheGeoFeed(IList<IPGeoFeed> pGeoFeeds)
{
string? tempPath = GetTempPath();
logger.LogInformation($"Writing geofeed data to: {tempPath}");
File.WriteAllText(tempPath, pGeoFeeds.ToList().
ToGeoFeedCsv());
return Task.FromResult(true);
}
private string GetTempPath()
{
string? tempPath = Path.Combine(Path.GetTempPath(), "geoFeedCache.csv");
logger.LogInformation($"Getting GeoFeed data from: {tempPath}");
if (!string.IsNullOrEmpty(configuration["TempCache"]))
tempPath = configuration["TempCache"];
return tempPath;
}
string IGeoFeedPersistentCacheProvider.GetGeoFeed()
{
return File.ReadAllText(GetTempPath());
}
}
}

View File

@@ -1,4 +1,5 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -40,7 +41,7 @@ namespace AS1024.GeoFeed.Core.GeoFeedPreloader
private async Task StartPreLoad()
{
logger.LogInformation("Preloading GeoFeed data in memory...");
List<Models.IPGeoFeed> feed = await provider.GetGeoFeedData();
List<IPGeoFeed> feed = await provider.GetGeoFeedData();
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(45));
memoryCache.Set(GeoFeedCacheKey, feed, cacheEntryOptions);
}

View File

@@ -1,129 +1,14 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using System.Text.Json;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Web;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.Core.GeoFeedProviders
{
public class NetBoxGeoFeedProvider : IGeoFeedProvider
public class NetBoxGeoFeedProvider : NetBoxGeoFeedProviderBase, IGeoFeedProvider
{
private readonly IConfiguration configuration;
private readonly ILogger<NetBoxGeoFeedProvider> logger;
private readonly IList<AddressFamily> addressFamilies = new List<AddressFamily>()
public NetBoxGeoFeedProvider(IConfiguration configuration, ILogger<NetBoxGeoFeedProvider> logger, IHttpClientFactory httpClientFactory)
: base(configuration, logger, httpClientFactory)
{
AddressFamily.InterNetwork,
AddressFamily.InterNetworkV6
};
private readonly IHttpClientFactory httpClientFactory;
string IGeoFeedProvider.GeoFeedProviderName => "netbox";
public NetBoxGeoFeedProvider(IConfiguration configuration,
ILogger<NetBoxGeoFeedProvider> logger,
IHttpClientFactory httpClientFactory)
{
this.configuration = configuration;
this.logger = logger;
this.httpClientFactory = httpClientFactory;
}
public async Task<List<IPGeoFeed>> GetGeoFeedData()
{
List<IPGeoFeed> geoFeed = [];
using HttpClient client = httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", configuration["APIKey"]);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
foreach (AddressFamily family in addressFamilies)
{
Uri uri = BuildNetBoxURI(family);
NetboxData? jsonData = null;
while (true)
{
logger.LogDebug($"Making request to {uri}...");
using HttpResponseMessage result = await client.GetAsync(uri);
if (!result.IsSuccessStatusCode)
{
break;
}
string stringResult = await result.Content.ReadAsStringAsync();
jsonData = JsonSerializer.Deserialize<NetboxData>(stringResult, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
if (jsonData?.Results == null || jsonData.Results.Count == 0)
{
break;
}
foreach (Result data in jsonData.Results)
{
try
{
geoFeed.Add(new IPGeoFeed
{
Prefix = data.Prefix,
GeolocCity = data.CustomFields.GeolocCity,
GeolocRegion = data.CustomFields.GeolocRegion,
GeolocCountry = data.CustomFields.GeolocCountry,
GeolocHasLocation = data.CustomFields.GeolocHasLocation,
GeolocPostalCode = data.CustomFields.GeolocPostalCode
});
}
catch (Exception ex)
{
logger.LogError(ex.ToString());
}
}
if (!string.IsNullOrEmpty(jsonData.Next))
{
uri = new Uri(jsonData.Next);
continue;
}
break;
}
}
return geoFeed;
}
protected Uri BuildNetBoxURI(AddressFamily family)
{
System.Collections.Specialized.NameValueCollection queryParameters = HttpUtility.ParseQueryString(string.Empty);
queryParameters["cf_geoloc_has_location"] = "true";
queryParameters["limit"] = "50";
switch (family)
{
case AddressFamily.InterNetwork:
queryParameters["mask_length__lte"] = "24";
queryParameters["family"] = "4";
break;
case AddressFamily.InterNetworkV6:
queryParameters["mask_length__lte"] = "48";
queryParameters["family"] = "6";
break;
}
UriBuilder endUrl = new UriBuilder
{
Path = "api/ipam/prefixes/",
Query = queryParameters.ToString(),
Host = configuration["NetBoxHost"],
Scheme = "https"
};
return endUrl.Uri;
}
}
}

View File

@@ -0,0 +1,134 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using System.Text.Json;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Web;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace AS1024.GeoFeed.Core.GeoFeedProviders
{
public abstract class NetBoxGeoFeedProviderBase : IGeoFeedProvider
{
protected readonly IConfiguration configuration;
protected readonly ILogger<NetBoxGeoFeedProvider> logger;
protected readonly IList<AddressFamily> addressFamilies = new List<AddressFamily>()
{
AddressFamily.InterNetwork,
AddressFamily.InterNetworkV6
};
protected readonly IHttpClientFactory httpClientFactory;
string IGeoFeedProvider.GeoFeedProviderName => "netbox";
public NetBoxGeoFeedProviderBase(IConfiguration configuration,
ILogger<NetBoxGeoFeedProvider> logger,
IHttpClientFactory httpClientFactory)
{
this.configuration = configuration;
this.logger = logger;
this.httpClientFactory = httpClientFactory;
}
public async Task<List<IPGeoFeed>> GetGeoFeedData()
{
List<IPGeoFeed> geoFeed = [];
using HttpClient client = httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", configuration["APIKey"]);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
foreach (AddressFamily family in addressFamilies)
{
Uri uri = BuildNetBoxURI(family);
NetboxData? jsonData = null;
while (true)
{
logger.LogDebug($"Making request to {uri}...");
using HttpResponseMessage result = await client.GetAsync(uri);
if (!result.IsSuccessStatusCode)
{
break;
}
string stringResult = await result.Content.ReadAsStringAsync();
jsonData = DeserializeJsonData(stringResult);
if (jsonData?.Results == null || jsonData.Results.Count == 0)
{
break;
}
foreach (Result data in jsonData.Results)
{
try
{
geoFeed.Add(new IPGeoFeed
{
Prefix = data.Prefix,
GeolocCity = data.CustomFields.GeolocCity,
GeolocRegion = data.CustomFields.GeolocRegion,
GeolocCountry = data.CustomFields.GeolocCountry,
GeolocHasLocation = data.CustomFields.GeolocHasLocation,
GeolocPostalCode = data.CustomFields.GeolocPostalCode
});
}
catch (Exception ex)
{
logger.LogError(ex.ToString());
}
}
if (!string.IsNullOrEmpty(jsonData.Next))
{
uri = new Uri(jsonData.Next);
continue;
}
break;
}
}
return geoFeed;
}
protected virtual NetboxData? DeserializeJsonData(string stringResult)
{
return JsonSerializer.Deserialize<NetboxData>(stringResult, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
}
protected Uri BuildNetBoxURI(AddressFamily family)
{
System.Collections.Specialized.NameValueCollection queryParameters = HttpUtility.ParseQueryString(string.Empty);
queryParameters["cf_geoloc_has_location"] = "true";
queryParameters["limit"] = "50";
switch (family)
{
case AddressFamily.InterNetwork:
queryParameters["mask_length__lte"] = "24";
queryParameters["family"] = "4";
break;
case AddressFamily.InterNetworkV6:
queryParameters["mask_length__lte"] = "48";
queryParameters["family"] = "6";
break;
}
UriBuilder endUrl = new UriBuilder
{
Path = "api/ipam/prefixes/",
Query = queryParameters.ToString(),
Host = configuration["NetBoxHost"],
Scheme = "https"
};
return endUrl.Uri;
}
}
}

View File

@@ -0,0 +1,31 @@
using AS1024.GeoFeed.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AS1024.GeoFeed.Core.Interfaces
{
/// <summary>
/// Represents a persistent cache GeoFeed provider
/// </summary>
public interface IGeoFeedPersistentCacheProvider
{
/// <summary>
/// Name of the provider
/// </summary>
public string ProviderName { get; }
/// <summary>
/// Returns the GeoFeed
/// </summary>
/// <returns>String of the CSV geofeed</returns>
public string GetGeoFeed();
/// <summary>
/// Stores the GeoFeed in the cache backend
/// </summary>
/// <param name="pGeoFeeds">GeoFeed retrieved from the backend</param>
/// <returns></returns>
public Task<bool> CacheGeoFeed(IList<IPGeoFeed> pGeoFeeds);
}
}

View File

@@ -13,4 +13,8 @@
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using AS1024.GeoFeed.Models;
using System.Text.Json.Serialization;
namespace AS1024.GeoFeed.MinimalAPI
{
[JsonSerializable(typeof(NetboxData))]
[JsonSerializable(typeof(Result))]
[JsonSerializable(typeof(CustomFields))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
}

View File

@@ -1,11 +1,11 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
@@ -20,9 +20,9 @@ RUN dotnet build "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true
RUN dotnet publish "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true /p:DebugType=None /p:DebugSymbols=false
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0 AS final
FROM base AS final
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .

View File

@@ -0,0 +1,55 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM alpine:latest AS base
ENV \
# UID of the non-root user 'app'
APP_UID=1654 \
# Configure web servers to bind to port 8080 when present
ASPNETCORE_HTTP_PORTS=8080 \
# Enable detection of running in a container
DOTNET_RUNNING_IN_CONTAINER=true \
# Set the invariant mode since ICU package isn't included (see https://github.com/dotnet/announcements/issues/20)
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true
#RUN apk add --upgrade --no-cache \
# ca-certificates-bundle \
# \
# # .NET dependencies
# libgcc \
# libssl3 \
# libstdc++ \
# zlib
# Create a non-root user and group
RUN addgroup \
--gid=$APP_UID \
app \
&& adduser \
--uid=$APP_UID \
--ingroup=app \
--disabled-password \
app
WORKDIR /app
USER app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apk add clang zlib-dev musl-dev libc6-compat
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["AS1024.GeoFeed.MinimalAPI/AS1024.GeoFeed.MinimalAPI.csproj", "AS1024.GeoFeed.MinimalAPI/"]
RUN dotnet restore "./AS1024.GeoFeed.MinimalAPI/./AS1024.GeoFeed.MinimalAPI.csproj"
COPY . .
WORKDIR "/src/AS1024.GeoFeed.MinimalAPI"
RUN dotnet build "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AS1024.GeoFeed.MinimalAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true /p:DebugType=None /p:DebugSymbols=false
FROM base AS final
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["./AS1024.GeoFeed.MinimalAPI"]

View File

@@ -0,0 +1,22 @@
using AS1024.GeoFeed.Core.GeoFeedProviders;
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Models;
using System.Text.Json;
namespace AS1024.GeoFeed.MinimalAPI
{
internal class NetboxAoTGeoFeedProvider : NetBoxGeoFeedProvider, IGeoFeedProvider
{
public NetboxAoTGeoFeedProvider(IConfiguration configuration,
ILogger<NetBoxGeoFeedProvider> logger,
IHttpClientFactory httpClientFactory)
: base(configuration, logger, httpClientFactory)
{
}
protected override NetboxData? DeserializeJsonData(string stringResult)
{
return JsonSerializer.Deserialize(stringResult, AppJsonSerializerContext.Default.NetboxData);
}
}
}

View File

@@ -1,18 +1,93 @@
using System.Text.Json.Serialization;
using AS1024.GeoFeed.Core.CacheService;
using AS1024.GeoFeed.Core.GeoFeedProviders;
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Core.Tools;
using AS1024.GeoFeed.Models;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Text;
namespace AS1024.GeoFeed.MinimalAPI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddTransient<IGeoFeedProvider, NetboxAoTGeoFeedProvider>();
builder.Services.AddHostedService<GeoFeedCacheService>();
builder.Services.AddTransient<IGeoFeedPersistentCacheProvider, GeoFeedLocalFileCache>();
builder.Services.AddMemoryCache();
builder.Services.AddLogging();
builder.Services.AddHttpClient();
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
var app = builder.Build();
var geoFeed = app.MapGroup("/geofeed.csv");
app.Map("/geofeed.csv", async (IGeoFeedProvider provider,
ILogger<Program> logger,
IGeoFeedPersistentCacheProvider cacheProvider,
IMemoryCache memoryCache,
IWebHostEnvironment environment) => {
return await GeoFeedDataRunner(provider, logger, cacheProvider, memoryCache, environment);
});
app.Map("/geofeed", async (IGeoFeedProvider provider,
ILogger<Program> logger,
IGeoFeedPersistentCacheProvider cacheProvider,
IMemoryCache memoryCache,
IWebHostEnvironment environment) => {
return await GeoFeedDataRunner(provider, logger, cacheProvider, memoryCache, environment);
});
app.Run();
}
}
protected static async Task<IResult> GeoFeedDataRunner(IGeoFeedProvider provider,
ILogger<Program> logger,
IGeoFeedPersistentCacheProvider cacheProvider,
IMemoryCache memoryCache,
IWebHostEnvironment environment)
{
try
{
if (!memoryCache.TryGetValue("Geofeed", out List<IPGeoFeed>? feed))
{
feed = await provider.GetGeoFeedData();
if (environment.IsProduction())
{
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(15));
memoryCache.Set("Geofeed", feed, cacheEntryOptions);
}
}
return Results.File(Encoding.UTF8.GetBytes(feed.ToGeoFeedCsv()),
"text/csv",
"geofeed.csv");
}
catch (HttpRequestException ex)
{
logger.LogWarning($"Temporary failure of retrieving GeoData from upstream. {ex}");
string geoFeedData = cacheProvider.GetGeoFeed();
return Results.File(Encoding.UTF8.GetBytes(geoFeedData),
"text/csv",
"geofeed.csv");
}
catch (Exception ex)
{
logger.LogError($"Error: {ex}");
}
return Results.NoContent();
}
}
}

View File

@@ -3,7 +3,7 @@
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "todos",
"launchUrl": "geofeed.csv",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},

View File

@@ -14,7 +14,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.Core", "AS10
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AS1024.GeoFeed.MinimalAPI", "AS1024.GeoFeed.MinimalAPI\AS1024.GeoFeed.MinimalAPI.csproj", "{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS1024.GeoFeed.Core.GeoFeedLocalCache", "AS1024.GeoFeed.Core.GeoFeedLocalCache\AS1024.GeoFeed.Core.GeoFeedLocalCache.csproj", "{C474F55D-AE8E-4DF3-A241-FB017551C74A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS1024.GeoFeed.Core.SqliteGeoFeedCache", "AS1024.GeoFeed.Core.SqliteGeoFeedCache\AS1024.GeoFeed.Core.SqliteGeoFeedCache.csproj", "{3459BB31-FA7A-44D1-872D-C5338ACFBF80}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -38,10 +38,10 @@ Global
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36F2958C-8D0E-463B-9BF3-D6E55E6FC0B8}.Release|Any CPU.Build.0 = Release|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C474F55D-AE8E-4DF3-A241-FB017551C74A}.Release|Any CPU.Build.0 = Release|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3459BB31-FA7A-44D1-872D-C5338ACFBF80}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AS1024.GeoFeed.Core.GeoFeedLocalCache\AS1024.GeoFeed.Core.GeoFeedLocalCache.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Core.SqliteGeoFeedCache\AS1024.GeoFeed.Core.SqliteGeoFeedCache.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Core\AS1024.GeoFeed.Core.csproj" />
<ProjectReference Include="..\AS1024.GeoFeed.Models\AS1024.GeoFeed.Models.csproj" />
</ItemGroup>

View File

@@ -3,7 +3,6 @@ using AS1024.GeoFeed.Core.Interfaces;
using Microsoft.Extensions.Caching.Memory;
using AS1024.GeoFeed.Models;
using System.Text;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using AS1024.GeoFeed.Core.Tools;
namespace AS1024.GeoFeed.Controllers
@@ -18,16 +17,16 @@ namespace AS1024.GeoFeed.Controllers
private readonly IMemoryCache memoryCache;
private readonly IWebHostEnvironment environment;
private readonly ILogger<GeofeedController> logger;
private readonly GeoFeedCacheDbContext dbContext;
private readonly IGeoFeedPersistentCacheProvider geoFeedPersistentCache;
private const string GeoFeedCacheKey = "GeoFeedData";
public GeofeedController(IGeoFeedProvider builder,
IMemoryCache memoryCache,
IWebHostEnvironment environment,
ILogger<GeofeedController> logger,
GeoFeedCacheDbContext dbContext) {
IGeoFeedPersistentCacheProvider geoFeedPersistentCache) {
this.logger = logger;
this.dbContext = dbContext;
this.geoFeedPersistentCache = geoFeedPersistentCache;
this.builder = builder;
this.memoryCache = memoryCache;
this.environment = environment;
@@ -54,10 +53,7 @@ namespace AS1024.GeoFeed.Controllers
} catch (HttpRequestException ex)
{
logger.LogWarning($"Temporary failure of retrieving GeoData from upstream. {ex}");
var results =
dbContext.GeoFeedCacheEntries.ToList();
List<IPGeoFeed> cachedData = [];
results.ForEach(cachedData.Add);
var cachedData = geoFeedPersistentCache.GetGeoFeed();
return ReturnFile(cachedData);
}
@@ -73,6 +69,12 @@ namespace AS1024.GeoFeed.Controllers
private IActionResult ReturnFile(List<IPGeoFeed>? feed)
{
string csvContent = feed.ToGeoFeedCsv(); // Assuming ToGeoFeedCsv() returns a string in CSV format.
return ReturnFile(csvContent);
}
[NonAction]
private IActionResult ReturnFile(string csvContent)
{
byte[] contentBytes = Encoding.UTF8.GetBytes(csvContent);
string contentType = "text/csv";

View File

@@ -1,8 +1,9 @@
using AS1024.GeoFeed.Core.Interfaces;
using AS1024.GeoFeed.Core.GeoFeedPreloader;
using AS1024.GeoFeed.Core.GeoFeedLocalCache;
using AS1024.GeoFeed.Core.GeoFeedProviders;
using Microsoft.EntityFrameworkCore;
using AS1024.GeoFeed.Core.GeoFeedSqliteLocalCache;
using AS1024.GeoFeed.Core.CacheService;
namespace AS1024.GeoFeed
{
@@ -14,11 +15,13 @@ namespace AS1024.GeoFeed
builder.Services.AddHostedService<PreLoadGeoFeed>();
builder.Services.AddTransient<IGeoFeedProvider, NetBoxGeoFeedProvider>();
builder.Services.AddDbContext<GeoFeedCacheDbContext>(
options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("LocalFeedCache"));
});
builder.Services.AddScoped<IGeoFeedPersistentCacheProvider, GeoFeedSqliteCache>();
builder.Services.AddHostedService<GeoFeedCacheService>();
builder.Services.AddDbContext<GeoFeedCacheDbContext>(options =>
{
options.UseSqlite(builder.Configuration.GetConnectionString("LocalFeedCache"));
});
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
// Add services to the container.

View File

@@ -4,25 +4,46 @@
This web application provides a self-published IP geolocation feed, conforming to the standards set out in [RFC 8805](https://www.rfc-editor.org/rfc/rfc8805). It is designed to interface with NetBox, a web-based infrastructure resource modeling (IRM) tool, to retrieve and format geolocation data for IP addresses.
The application is implemented in C# using .NET 8.0, ensuring a robust and modern back-end architecture. It's built to communicate securely over HTTPS with the NetBox API, adhering to best practices for data transmission and security.
The application is implemented in C# using .NET 8.0, ensuring a robust and modern back-end architecture. It's built to communicate securely over HTTPS with the NetBox API, adhering to best practices for data transmission and security.
This project was inspired by the GitHub project [GeoBox](https://github.com/FrumentumNL/GeoBox), which provided foundational ideas for the development of this application.
## Application Variants
### AS1024.GeoFeed - MVC Version (Recommended)
The standard version of this application, named `AS1024.GeoFeed`, is built using the Model-View-Controller (MVC) architecture. This variant is fully supported and recommended for most use cases. It offers a complete set of features and is optimized for robustness and scalability.
### AS1024.GeoFeed.MinimalAPI - MinimalAPI Version (Experimental)
In addition to the standard MVC version, there is an experimental MinimalAPI version of the application named `AS1024.GeoFeed.MinimalAPI`. This variant is designed for environments that require extremely fast startup times, such as serverless containers. However, it comes with limited support and a reduced feature set. We recommend using the `AS1024.GeoFeed.MinimalAPI` version only if your deployment environment necessitates near-instant startup times and you are comfortable with its experimental nature and limitations.
**Note**: While the `AS1024.GeoFeed.MinimalAPI` version offers performance benefits in specific scenarios, we strongly recommend deploying the `AS1024.GeoFeed` MVC version for most applications to ensure full functionality and support.
## Features
- **GeoFeed Generation**: Dynamically generates a geolocation feed in CSV format as specified in RFC 8805.
- **Caching Mechanism**: Implements an efficient caching strategy to reduce redundant API calls and enhance performance.
- **Local Disk Fallback Caching Mechanism**: In the event of a failure to communicate with the NetBox instance specified, the web app will return data that is locally cached inside a SQLite database.
- **Local Disk Fallback Caching Mechanism**: In the event of a failure to communicate with the NetBox instance specified, the web app will return data that is locally cached inside a SQLite database. In the minimal API version of the GeoFeed application, this will be stored as a file, rather than a SQLite database as Minimal API does not yet support Entity Framework Core at this time.
- **Secure Communication**: Ensures secure data retrieval from NetBox over HTTPS.
## Configuration
The application requires the following configuration variables to be set:
The application requires the following configuration variables to be set, depending on the version you are using:
### For AS1024.GeoFeed - MVC Version
1. **APIKey**: This is the API key used for authenticating with the NetBox API. Ensure this key has the necessary permissions to access the required resources.
2. **NetBoxHost**: The hostname of the NetBox instance from which the application retrieves data. For example, `netbox.example.com`.
3. **LocalFeedCache**: This connection string is for a local SQLite Database that caches the geofeed data from Netbox.
3. **LocalFeedCache**: This connection string is for a local SQLite Database (using EF Core) that caches the geofeed data from Netbox. The syntax should follow the standard SQLite EF Core connection string format.
### For AS1024.GeoFeed.MinimalAPI - MinimalAPI Version
1. **APIKey**: This is the API key used for authenticating with the NetBox API. Ensure this key has the necessary permissions to access the required resources.
2. **NetBoxHost**: The hostname of the NetBox instance from which the application retrieves data. For example, `netbox.example.com`.
3. **TempCache** (optional): This configuration is specific to the Minimal API version. It is optional, but if users wish to specify a different location for storing and serving the cached geofeed data, this value can be adjusted to point to the desired save location.
These variables can be set in your application's configuration file or through environment variables, depending on your deployment strategy.
## NetBox Custom Fields
Ensure that your NetBox instance is configured with the following custom fields:
@@ -46,6 +67,37 @@ To build and run the application, follow these steps:
5. After a successful build, you can start the application by running `dotnet run`.
6. The application will start, and you can access the endpoints as specified.
## Building a Docker Image
### Building a Docker Image for the Minimal API Version
To containerize the `AS1024.GeoFeed` version using Docker, you can follow these steps to build a Docker image:
1. Open your terminal or command line interface.
2. Navigate to the root directory of this repository.
3. Execute the following Docker build command: ``docker buildx build --platform=linux/amd64,linux/arm64 -f .\AS1024.GeoFeed\Dockerfile .``
### Building a Docker Image for the Minimal API Version
To containerize the `AS1024.GeoFeed.MinimalAPI` version using Docker, you can follow these steps to build a Docker image:
1. Open your terminal or command line interface.
2. Navigate to the root directory of this repository.
3. Execute the following Docker build command: ``docker buildx build --platform=linux/amd64 -f .\AS1024.GeoFeed.MinimalAPI\Dockerfile.alpine-selfcontained .``
**Currently the minimal API version does not support being cross compiled for different CPU architectures. It is best built with the same CPU architecture where the build machine is targeting to.**
Ensure you have Docker installed and configured on your machine before executing this command. This Docker image can then be used to deploy the application in containerized environments such as Kubernetes or Docker Compose setups.
## Docker Deployment
To deploy the Docker container of this GeoFeed application, simply pull the image for the following variants:
- MVC Variant ``docker pull git.startmywifi.com/as1024/geofeed:latest``
- Minimal API Variant ``docker pull git.startmywifi.com/as1024/geofeed:aot-minimal``
To configure the container, please refer to the Configuration section above. You should be able to configure this with either an environment variable or map an ``appSettings.json`` file to the container.
## Endpoints
The application provides the following key endpoints: